{
  "metadata": {
    "id": "ch26",
    "title": "第26章：智能客服系统",
    "volume": "vol8",
    "volume_title": "实战案例集",
    "word_count": 2309,
    "difficulty": "advanced",
    "prerequisites": [
      "ch06",
      "ch07",
      "ch08"
    ],
    "key_concepts": [
      "需求分析与功能规划",
      "业务背景",
      "功能清单",
      "非功能需求",
      "架构设计",
      "项目结构",
      "核心类设计",
      "核心代码实现",
      "项目配置",
      "意图识别 Agent",
      "情感分析 Agent",
      "知识检索 Agent",
      "对话管理 Agent（核心）",
      "工单系统",
      "人机协作管理"
    ],
    "learning_objectives": [],
    "estimated_tokens": 1385,
    "source_file": "vol8/ch26_智能客服系统.md"
  },
  "overview": "",
  "sections": [
    {
      "id": "26.1",
      "title": "26.1 需求分析与功能规划",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "26.1.1",
          "title": "26.1.1 业务背景",
          "content": "传统客服系统存在三大痛点：\n\n1. **响应慢**：人工客服平均响应时间 3-5 分钟，高峰期排队可达 30 分钟\n2. **成本高**：一个成熟的客服团队年成本超过 200 万元\n3. **知识流失**：客服人员流动导致产品知识无法沉淀\n\n基于这些痛点，我们需要构建一个智能客服系统，实现：\n\n- **70% 的常见问题自动解答**，减少人工介入\n- **24/7 全天候服务**，消除时间限制\n- **知识库驱动**的精准回答，而非模板化的固定话术\n- **平滑的人机协作**，复杂问题无缝转交人工"
        },
        {
          "id": "26.1.2",
          "title": "26.1.2 功能清单",
          "content": ""
        },
        {
          "id": "26.1.3",
          "title": "26.1.3 非功能需求",
          "content": "| 维度 | 指标 |\n|------|------|\n| 响应时间 | P95 < 2 秒 |\n| 并发支持 | 500 QPS |\n| 可用性 | 99.9% |\n| 知识库容量 | 10 万+ FAQ 条目 |\n| 多轮对话深度 | 最多 20 轮 |\n\n---"
        }
      ]
    },
    {
      "id": "26.2",
      "title": "26.2 架构设计",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "26.2.1",
          "title": "26.2.1 项目结构",
          "content": ""
        },
        {
          "id": "26.2.2",
          "title": "26.2.2 核心类设计",
          "content": "系统由五个 Agent 组成，DialogAgent 作为核心协调者：\n\n- **IntentAgent**：理解用户想做什么（查订单、问FAQ、投诉等）\n- **EmotionAgent**：识别用户情绪（愤怒、焦虑、满意等）\n- **KnowledgeAgent**：从知识库中找到最相关的答案\n- **DialogAgent**：管理多轮对话流程，协调其他 Agent\n\n**设计决策**：每个 Agent 职责单一，可独立测试和升级。DialogAgent 负责调度和整合，避免 Agent 之间的直接耦合。\n\n---"
        }
      ]
    },
    {
      "id": "26.3",
      "title": "26.3 核心代码实现",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "26.3.1",
          "title": "26.3.1 项目配置",
          "content": ""
        },
        {
          "id": "26.3.2",
          "title": "26.3.2 意图识别 Agent",
          "content": "**设计要点**：Few-Shot Prompting 提高分类准确率；降级策略确保 LLM 不可用时仍可工作；上下文感知利用前轮对话辅助判断。"
        },
        {
          "id": "26.3.3",
          "title": "26.3.3 情感分析 Agent",
          "content": "情感分析的核心价值不仅是理解用户，更是**驱动转人工决策**。愤怒情绪触发优先分配经验丰富的坐席，避免矛盾激化。"
        },
        {
          "id": "26.3.4",
          "title": "26.3.4 知识检索 Agent",
          "content": ""
        },
        {
          "id": "26.3.5",
          "title": "26.3.5 对话管理 Agent（核心）",
          "content": "DialogAgent 是整个系统的\"大脑\"，负责协调所有子 Agent 并管理对话流程。"
        },
        {
          "id": "26.3.6",
          "title": "26.3.6 工单系统",
          "content": ""
        },
        {
          "id": "26.3.7",
          "title": "26.3.7 人机协作管理",
          "content": ""
        },
        {
          "id": "26.3.8",
          "title": "26.3.8 FastAPI 应用入口",
          "content": "---"
        }
      ]
    },
    {
      "id": "26.4",
      "title": "26.4 测试",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "26.4.1",
          "title": "26.4.1 意图识别测试",
          "content": ""
        },
        {
          "id": "26.4.2",
          "title": "26.4.2 对话管理测试",
          "content": ""
        },
        {
          "id": "26.4.3",
          "title": "26.4.3 API 集成测试",
          "content": "---"
        }
      ]
    },
    {
      "id": "26.5",
      "title": "26.5 部署",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "26.5.1",
          "title": "26.5.1 Docker 部署",
          "content": "---"
        }
      ]
    },
    {
      "id": "26.6",
      "title": "26.6 经验总结",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "26.6.1",
          "title": "26.6.1 踩坑记录",
          "content": "**坑1：意图分类的边界模糊**\n\n\"你们的产品质量有问题\"到底是 FAQ 还是投诉？解决方案是引入**意图优先级**：complaint > refund > faq。当多个意图可能匹配时，优先选择用户利益相关度更高的意图。同时利用多轮对话的上下文来消歧——如果用户之前在讨论产品质量，则倾向于投诉。\n\n**坑2：知识库匹配的\"幻觉\"问题**\n\nLLM 有时会\"编造\"知识库中不存在的信息。我们的解决方案是严格的 Prompt 约束：\"只基于提供的知识库内容回答，不编造信息\"，同时在回答末尾附加\"信息来源\"标注，方便用户核实。\n\n**坑3：转人工时机的选择**\n\n过早转人工浪费人工资源，过晚则激化用户情绪。我们采用**多信号融合决策**：\n- 意图信号：明确要求转人工\n- 情绪信号：连续 3 轮负面情绪\n- 效率信号：对话超过 20 轮仍未解决\n- 紧急信号：涉及大额金额或法律威胁\n\n四个信号中满足任意一个即触发转人工。\n\n**坑4：并发会话的上下文隔离**\n\n多个用户同时对话时，上下文串台是一个严重问题。我们通过 `session_id` 做严格的命名空间隔离，并在内存中使用独立字典存储每个会话的状态。"
        },
        {
          "id": "26.6.2",
          "title": "26.6.2 性能优化经验",
          "content": "1. **意图识别和情感分析并行执行**：使用 `asyncio.gather` 将两个 LLM 调用并发化，响应时间减少约 40%\n2. **知识库检索使用 ChromaDB 持久化缓存**：避免每次启动重新加载向量索引\n3. **Few-Shot 示例预编译**：在 Agent 初始化时构建 prompt 模板，避免每次请求重新格式化\n4. **WebSocket 长连接**：减少 HTTP 握手开销，实现流式输出"
        },
        {
          "id": "26.6.3",
          "title": "26.6.3 关键设计模式总结",
          "content": "| 模式 | 应用场景 | 效果 |\n|------|---------|------|\n| 优雅降级 | LLM 不可用时回退到规则匹配 | 系统可用性从 99% 提升到 99.9% |\n| 多信号融合 | 转人工决策 | 减少误判 60% |\n| 技能路由 | 坐席分配 | 客户满意度提升 25% |\n| 知识增强生成 | FAQ 回答 | 回答准确率 92% → 96% |"
        },
        {
          "id": "26.6.4",
          "title": "26.6.4 未来演进方向",
          "content": "1. **多模态支持**：接入图片识别（用户上传故障截图）、语音输入\n2. **主动服务**：基于用户行为预测问题，提前推送解决方案\n3. **学习型知识库**：从人工客服的处理记录中自动提炼新 FAQ\n4. **个性化体验**：根据用户画像调整回答风格和专业深度\n\n---\n\n**本章小结**：智能客服系统是 Agent 技术最成熟的应用场景之一。关键在于**多 Agent 协调**（意图、情感、知识各司其职）和**人机平滑切换**（知道何时该出手、何时该退让）。通过合理的架构设计和降级策略，可以构建出既智能又可靠的客服系统。"
        }
      ]
    }
  ],
  "code_blocks": [
    {
      "id": "code-1",
      "language": "text",
      "description": "- 平滑的人机协作，复杂问题无缝转交人工",
      "code": "┌─────────────────────────────────────────────────┐\n│                 智能客服系统功能架构               │\n├─────────────────────────────────────────────────┤\n│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐ │\n│  │ 用户接入层   │  │  对话引擎层  │  │ 业务层   │ │\n│  │ • Web 聊天  │  │ • 意图识别  │  │ • 知识库  │ │\n│  │ • 微信对接  │  │ • 多轮管理  │  │ • 工单系统│ │\n│  │ • API 接口  │  │ • 情感分析  │  │ • 用户画像│ │\n│  │ • 电话语音  │  │ • 回答生成  │  │ • 数据统计│ │\n│  └─────────────┘  └─────────────┘  └──────────┘ │\n│  ┌─────────────────────────────────────────────┐ │\n│  │              人机协作层                       │ │\n│  │  • 转人工决策  • 工单创建  • 坐席分配        │ │\n│  └─────────────────────────────────────────────┘ │\n└─────────────────────────────────────────────────┘",
      "section_ref": "26.1.2",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-2",
      "language": "text",
      "description": "",
      "code": "smart-customer-service/\n├── app/\n│   ├── main.py                # FastAPI 入口\n│   ├── config.py              # 配置管理\n│   ├── models/                # 数据模型\n│   │   ├── message.py\n│   │   ├── session.py\n│   │   └── ticket.py          # 工单模型\n│   ├── agents/                # Agent 核心\n│   │   ├── intent_agent.py    # 意图识别\n│   │   ├── dialog_agent.py    # 对话管理\n│   │   ├── emotion_agent.py   # 情感分析\n│   │   └── knowledge_agent.py # 知识检索\n│   ├── services/              # 业务服务\n│   │   ├── ticket_service.py\n│   │   └── human_handoff.py   # 人机协作\n│   └── utils/\n│       └── llm_client.py      # LLM 客户端\n├── tests/\n├── docker-compose.yml\n└── requirements.txt",
      "section_ref": "26.2.1",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-3",
      "language": "python",
      "description": "",
      "code": "# app/config.py\n\"\"\"智能客服系统配置管理\"\"\"\n\nfrom pydantic_settings import BaseSettings\nfrom enum import Enum\n\n\nclass LLMProvider(str, Enum):\n    OPENAI = \"openai\"\n    CLAUDE = \"claude\"\n    GLM = \"glm\"\n\n\nclass Settings(BaseSettings):\n    APP_NAME: str = \"智能客服系统\"\n    APP_VERSION: str = \"1.0.0\"\n    DEBUG: bool = False\n    \n    # LLM 配置\n    LLM_PROVIDER: LLMProvider = LLMProvider.OPENAI\n    LLM_API_KEY: str = \"\"\n    LLM_BASE_URL: str = \"https://api.openai.com/v1\"\n    LLM_MODEL: str = \"gpt-4o\"\n    LLM_TEMPERATURE: float = 0.3\n    LLM_MAX_TOKENS: int = 2048\n    \n    # 向量数据库\n    CHROMA_PERSIST_DIR: str = \"./chroma_data\"\n    EMBEDDING_MODEL: str = \"text-embedding-3-small\"\n    \n    # 业务参数\n    MAX_DIALOG_TURNS: int = 20\n    KNOWLEDGE_SIMILARITY_THRESHOLD: float = 0.75\n    MAX_KNOWLEDGE_RESULTS: int = 5\n    HUMAN_HANDOFF_TIMEOUT: int = 60\n    \n    class Config:\n        env_file = \".env\"\n        env_prefix = \"CS_\"\n\n\nsettings = Settings()",
      "section_ref": "26.3.1",
      "runnable": true,
      "dependencies": [
        "pydantic_settings"
      ]
    },
    {
      "id": "code-4",
      "language": "python",
      "description": "settings = Settings()",
      "code": "# app/utils/llm_client.py\n\"\"\"LLM 客户端封装\"\"\"\n\nimport json\nfrom typing import Optional, List, Dict\nfrom openai import OpenAI\nfrom app.config import settings\n\n\nclass LLMClient:\n    _instance: Optional['LLMClient'] = None\n    \n    def __new__(cls) -> 'LLMClient':\n        if cls._instance is None:\n            cls._instance = super().__new__(cls)\n            cls._instance._client = OpenAI(\n                api_key=settings.LLM_API_KEY,\n                base_url=settings.LLM_BASE_URL,\n            )\n        return cls._instance\n    \n    async def chat(\n        self,\n        messages: List[Dict[str, str]],\n        system_prompt: Optional[str] = None,\n        temperature: Optional[float] = None,\n        max_tokens: Optional[int] = None,\n        response_format: Optional[dict] = None,\n    ) -> str:\n        full_messages = []\n        if system_prompt:\n            full_messages.append({\"role\": \"system\", \"content\": system_prompt})\n        full_messages.extend(messages)\n        \n        kwargs = {\n            \"model\": settings.LLM_MODEL,\n            \"messages\": full_messages,\n            \"temperature\": temperature or settings.LLM_TEMPERATURE,\n            \"max_tokens\": max_tokens or settings.LLM_MAX_TOKENS,\n        }\n        if response_format:\n            kwargs[\"response_format\"] = response_format\n        \n        response = self._client.chat.completions.create(**kwargs)\n        return response.choices[0].message.content\n    \n    async def chat_json(\n        self, messages: List[Dict[str, str]],\n        system_prompt: Optional[str] = None,\n    ) -> dict:\n        content = await self.chat(\n            messages=messages, system_prompt=system_prompt,\n            temperature=0.1,\n            response_format={\"type\": \"json_object\"},\n        )\n        return json.loads(content)\n    \n    async def embed(self, texts: List[str]) -> List[List[float]]:\n        response = self._client.embeddings.create(\n            model=settings.EMBEDDING_MODEL, input=texts,\n        )\n        return [item.embedding for item in response.data]\n\n\nllm_client = LLMClient()",
      "section_ref": "26.3.1",
      "runnable": true,
      "dependencies": [
        "openai",
        "app"
      ]
    },
    {
      "id": "code-5",
      "language": "python",
      "description": "",
      "code": "# app/agents/intent_agent.py\n\"\"\"意图识别 Agent\"\"\"\n\nimport json\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nfrom typing import Optional, List\nfrom app.utils.llm_client import llm_client\n\n\nclass IntentType(str, Enum):\n    FAQ = \"faq\"\n    PRODUCT_INQUIRY = \"product\"\n    ORDER_QUERY = \"order\"\n    COMPLAINT = \"complaint\"\n    REFUND = \"refund\"\n    TECHNICAL = \"technical\"\n    ACCOUNT = \"account\"\n    TRANSFER_HUMAN = \"transfer\"\n    GREETING = \"greeting\"\n    UNKNOWN = \"unknown\"\n\n\n@dataclass\nclass IntentResult:\n    intent: IntentType\n    confidence: float\n    entities: dict = field(default_factory=dict)\n    sub_intent: Optional[str] = None\n    raw_text: str = \"\"\n\n\nclass IntentAgent:\n    SYSTEM_PROMPT = \"\"\"你是一个智能客服意图识别模块。\n分析用户输入，识别意图并提取关键实体。\n\n意图类型：\n- faq: 常见问题 | product: 产品查询 | order: 订单查询\n- complaint: 投诉 | refund: 退款退货 | technical: 技术支持\n- account: 账户问题 | transfer: 转人工 | greeting: 问候\n- unknown: 无法识别\n\n返回 JSON：{\"intent\":\"...\",\"confidence\":0.0-1.0,\"entities\":{}}\"\"\"\n\n    def __init__(self):\n        self._examples = [\n            {\"text\": \"我的订单 ORD-2024-1234 什么时候发货？\",\n             \"intent\": \"order\", \"confidence\": 0.95,\n             \"entities\": {\"order_id\": \"ORD-2024-1234\"}},\n            {\"text\": \"你们支持花呗付款吗？\",\n             \"intent\": \"faq\", \"confidence\": 0.90, \"entities\": {}},\n            {\"text\": \"我要投诉，你们的快递太慢了！\",\n             \"intent\": \"complaint\", \"confidence\": 0.95,\n             \"entities\": {\"reason\": \"快递太慢\"}},\n            {\"text\": \"这个耳机买了三天就坏了，我要退货退款\",\n             \"intent\": \"refund\", \"confidence\": 0.92,\n             \"entities\": {\"product_name\": \"耳机\", \"reason\": \"质量问题\"}},\n            {\"text\": \"App一打开就闪退，怎么办？\",\n             \"intent\": \"technical\", \"confidence\": 0.90, \"entities\": {}},\n            {\"text\": \"我忘了密码，手机号也换了\",\n             \"intent\": \"account\", \"confidence\": 0.93,\n             \"entities\": {\"reason\": \"忘记密码，手机号更换\"}},\n            {\"text\": \"转人工！\",\n             \"intent\": \"transfer\", \"confidence\": 0.98, \"entities\": {}},\n        ]\n    \n    async def classify(\n        self, text: str, context: Optional[dict] = None\n    ) -> IntentResult:\n        few_shot = []\n        for ex in self._examples:\n            few_shot.append({\"role\": \"user\", \"content\": ex[\"text\"]})\n            few_shot.append({\"role\": \"assistant\", \"content\": json.dumps(\n                {\"intent\": ex[\"intent\"], \"confidence\": ex[\"confidence\"],\n                 \"entities\": ex[\"entities\"]}, ensure_ascii=False)})\n        \n        ctx = \"\"\n        if context and context.get(\"last_intent\"):\n            ctx = f\"上一轮意图: {context['last_intent']}\\n\"\n        \n        try:\n            result = await llm_client.chat_json(\n                messages=few_shot + [{\"role\": \"user\",\n                    \"content\": f\"{ctx}用户输入: {text}\"}],\n                system_prompt=self.SYSTEM_PROMPT,\n            )\n            return IntentResult(\n                intent=IntentType(result.get(\"intent\", \"unknown\")),\n                confidence=float(result.get(\"confidence\", 0.5)),\n                entities=result.get(\"entities\", {}),\n                raw_text=text,\n            )\n        except Exception:\n            return self._fallback(text)\n    \n    def _fallback(self, text: str) -> IntentResult:\n        \"\"\"降级：关键词规则匹配\"\"\"\n        t = text.lower()\n        rules = {\n            IntentType.TRANSFER_HUMAN: [\"转人工\", \"人工客服\"],\n            IntentType.ORDER_QUERY: [\"订单\", \"发货\", \"物流\", \"快递\"],\n            IntentType.COMPLAINT: [\"投诉\", \"太差\", \"垃圾\"],\n            IntentType.REFUND: [\"退货\", \"退款\", \"换货\"],\n            IntentType.ACCOUNT: [\"密码\", \"账号\", \"登录\"],\n            IntentType.TECHNICAL: [\"闪退\", \"bug\", \"报错\", \"故障\"],\n            IntentType.GREETING: [\"你好\", \"在吗\", \"hello\"],\n        }\n        for intent, kws in rules.items():\n            if any(k in t for k in kws):\n                return IntentResult(intent=intent, confidence=0.6,\n                                    entities={}, raw_text=text)\n        return IntentResult(intent=IntentType.UNKNOWN, confidence=0.3,\n                            entities={}, raw_text=text)",
      "section_ref": "26.3.2",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-6",
      "language": "python",
      "description": "设计要点：Few-Shot Prompting 提高分类准确率；降级策略确保 LLM 不可用时仍可工作；上下文感知利用前轮对话辅助判断。",
      "code": "# app/agents/emotion_agent.py\n\"\"\"情感分析 Agent\"\"\"\n\nimport json\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom app.utils.llm_client import llm_client\n\n\nclass EmotionType(str, Enum):\n    POSITIVE = \"positive\"\n    NEUTRAL = \"neutral\"\n    SLIGHT_NEGATIVE = \"slightly_negative\"\n    NEGATIVE = \"negative\"\n    ANGRY = \"angry\"\n    ANXIOUS = \"anxious\"\n    CONFUSED = \"confused\"\n\n\n@dataclass\nclass EmotionResult:\n    emotion: EmotionType\n    intensity: float\n    confidence: float\n    urgency: bool\n\n\nclass EmotionAgent:\n    SYSTEM_PROMPT = \"\"\"你是客户情感分析专家。\n\n情绪类型：positive|neutral|slightly_negative|negative|angry|anxious|confused\nurgency：大额退款/投诉/曝光/法律手段/辱骂 → true\n\n返回 JSON：{\"emotion\":\"...\",\"intensity\":0.0-1.0,\n            \"confidence\":0.0-1.0,\"urgency\":bool}\"\"\"\n\n    async def analyze(self, text: str) -> EmotionResult:\n        try:\n            result = await llm_client.chat_json(\n                messages=[{\"role\": \"user\",\n                          \"content\": f\"分析情绪：\\n{text}\"}],\n                system_prompt=self.SYSTEM_PROMPT,\n            )\n            return EmotionResult(\n                emotion=EmotionType(result.get(\"emotion\", \"neutral\")),\n                intensity=float(result.get(\"intensity\", 0.5)),\n                confidence=float(result.get(\"confidence\", 0.7)),\n                urgency=result.get(\"urgency\", False),\n            )\n        except Exception:\n            return self._fallback(text)\n    \n    def _fallback(self, text: str) -> EmotionResult:\n        t = text.lower()\n        if any(w in t for w in [\"垃圾\",\"骗子\",\"投诉\",\"曝光\",\"律师\"]):\n            return EmotionResult(EmotionType.ANGRY, 0.8, 0.7, True)\n        if any(w in t for w in [\"不满\",\"差\",\"慢\",\"失望\"]):\n            return EmotionResult(EmotionType.NEGATIVE, 0.6, 0.6, False)\n        if any(w in t for w in [\"急\",\"什么时候\",\"赶紧\"]):\n            return EmotionResult(EmotionType.ANXIOUS, 0.5, 0.6, False)\n        if any(w in t for w in [\"谢谢\",\"满意\",\"棒\"]):\n            return EmotionResult(EmotionType.POSITIVE, 0.5, 0.6, False)\n        return EmotionResult(EmotionType.NEUTRAL, 0.3, 0.8, False)",
      "section_ref": "26.3.3",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-7",
      "language": "python",
      "description": "情感分析的核心价值不仅是理解用户，更是驱动转人工决策。愤怒情绪触发优先分配经验丰富的坐席，避免矛盾激化。",
      "code": "# app/agents/knowledge_agent.py\n\"\"\"知识检索 Agent\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\nimport chromadb\nfrom app.config import settings\nfrom app.utils.llm_client import llm_client\n\n\n@dataclass\nclass KnowledgeItem:\n    id: str\n    question: str\n    answer: str\n    category: str\n    tags: List[str] = field(default_factory=list)\n    score: float = 0.0\n    source: str = \"\"\n\n\n@dataclass\nclass SearchResult:\n    items: List[KnowledgeItem]\n    query: str\n    has_answer: bool = False\n\n\nclass KnowledgeAgent:\n    def __init__(self):\n        self._client = chromadb.PersistentClient(\n            path=settings.CHROMA_PERSIST_DIR)\n        try:\n            self._collection = self._client.get_collection(\"cs_kb\")\n        except Exception:\n            self._collection = self._client.create_collection(\"cs_kb\")\n    \n    async def search(\n        self, query: str,\n        category: Optional[str] = None, top_k: int = 5,\n    ) -> SearchResult:\n        where = {\"category\": category} if category else None\n        results = self._collection.query(\n            query_texts=[query], n_results=top_k, where=where)\n        \n        items = []\n        if results and results[\"ids\"] and results[\"ids\"][0]:\n            for i, doc_id in enumerate(results[\"ids\"][0]):\n                meta = results[\"metadatas\"][0][i]\n                sim = 1 - results[\"distances\"][0][i]\n                if sim >= settings.KNOWLEDGE_SIMILARITY_THRESHOLD:\n                    items.append(KnowledgeItem(\n                        id=doc_id,\n                        question=meta.get(\"question\", \"\"),\n                        answer=results[\"documents\"][0][i],\n                        category=meta.get(\"category\", \"\"),\n                        score=sim,\n                    ))\n        \n        return SearchResult(\n            items=items, query=query,\n            has_answer=any(it.score >= 0.85 for it in items))\n    \n    async def generate_answer(self, query: str, sr: SearchResult) -> str:\n        if not sr.items:\n            return \"抱歉，我暂时无法回答这个问题，建议转接人工客服。\"\n        \n        ctx = \"\\n---\\n\".join(\n            f\"Q: {it.question}\\nA: {it.answer}\" for it in sr.items)\n        \n        return await llm_client.chat(\n            messages=[{\"role\": \"user\", \"content\":\n                f\"知识库：\\n{ctx}\\n\\n用户问题：{query}\"}],\n            system_prompt=\"你是专业客服。只基于知识库回答，简洁专业。\",\n            temperature=0.3,\n        ).strip()\n    \n    def add_knowledge(\n        self, question: str, answer: str,\n        category: str = \"general\",\n        tags: Optional[List[str]] = None, source: str = \"\",\n    ) -> str:\n        import uuid\n        doc_id = str(uuid.uuid4())\n        self._collection.add(\n            documents=[answer],\n            metadatas=[{\"question\": question, \"category\": category,\n                        \"tags\": tags or [], \"source\": source}],\n            ids=[doc_id])\n        return doc_id",
      "section_ref": "26.3.4",
      "runnable": true,
      "dependencies": [
        "chromadb",
        "app"
      ]
    },
    {
      "id": "code-8",
      "language": "python",
      "description": "DialogAgent 是整个系统的\"大脑\"，负责协调所有子 Agent 并管理对话流程。",
      "code": "# app/agents/dialog_agent.py\n\"\"\"对话管理 Agent - 系统核心\"\"\"\n\nimport json\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List, Dict, Any\nfrom datetime import datetime\nfrom app.agents.intent_agent import IntentAgent, IntentResult\nfrom app.agents.emotion_agent import EmotionAgent, EmotionResult\nfrom app.agents.knowledge_agent import KnowledgeAgent\nfrom app.config import settings\n\n\n@dataclass\nclass DialogTurn:\n    role: str\n    content: str\n    timestamp: datetime = field(default_factory=datetime.now)\n    intent: Optional[str] = None\n    emotion: Optional[str] = None\n\n\n@dataclass\nclass DialogContext:\n    session_id: str\n    user_id: str\n    turns: List[DialogTurn] = field(default_factory=list)\n    current_intent: Optional[str] = None\n    current_emotion: Optional[str] = None\n    entities: Dict[str, Any] = field(default_factory=dict)\n    metadata: Dict[str, Any] = field(default_factory=dict)\n    \n    def add_turn(self, role: str, content: str, **kwargs):\n        self.turns.append(DialogTurn(role=role, content=content, **kwargs))\n    \n    def get_history_text(self, max_turns: int = 10) -> str:\n        recent = self.turns[-max_turns:]\n        return \"\\n\".join(\n            f\"{'用户' if t.role == 'user' else '客服'}: {t.content}\"\n            for t in recent)\n    \n    @property\n    def turn_count(self) -> int:\n        return sum(1 for t in self.turns if t.role == \"user\")\n\n\n@dataclass\nclass AgentResponse:\n    content: str\n    intent: str\n    emotion: str\n    confidence: float\n    should_escalate: bool = False\n    escalate_reason: str = \"\"\n    suggested_actions: List[str] = field(default_factory=list)\n\n\nclass DialogAgent:\n    def __init__(self):\n        self._intent = IntentAgent()\n        self._emotion = EmotionAgent()\n        self._knowledge = KnowledgeAgent()\n    \n    async def process(\n        self, user_msg: str, ctx: DialogContext\n    ) -> AgentResponse:\n        \"\"\"处理用户消息\"\"\"\n        import asyncio\n        \n        # 并行执行意图识别和情感分析\n        intent_r, emotion_r = await asyncio.gather(\n            self._intent.classify(user_msg,\n                {\"last_intent\": ctx.current_intent}),\n            self._emotion.analyze(user_msg),\n        )\n        \n        ctx.add_turn(\"user\", user_msg,\n                     intent=intent_r.intent.value,\n                     emotion=emotion_r.emotion.value)\n        ctx.current_intent = intent_r.intent.value\n        ctx.current_emotion = emotion_r.emotion.value\n        ctx.entities.update(intent_r.entities)\n        \n        # 检查是否需要转人工\n        esc = self._check_escalation(ctx, intent_r, emotion_r)\n        if esc:\n            return AgentResponse(\n                content=self._esc_msg(emotion_r),\n                intent=intent_r.intent.value,\n                emotion=emotion_r.emotion.value,\n                confidence=1.0,\n                should_escalate=True,\n                escalate_reason=esc,\n            )\n        \n        # 路由处理\n        reply = await self._route(intent_r, emotion_r, ctx)\n        ctx.add_turn(\"assistant\", reply)\n        \n        # 多轮兜底提示\n        if ctx.turn_count >= settings.MAX_DIALOG_TURNS:\n            reply += \"\\n\\n您的问题较复杂，建议转接人工客服。\"\n        \n        return AgentResponse(\n            content=reply,\n            intent=intent_r.intent.value,\n            emotion=emotion_r.emotion.value,\n            confidence=intent_r.confidence,\n        )\n    \n    def _check_escalation(self, ctx, intent, emotion) -> str | None:\n        if intent.intent.value == \"transfer\":\n            return \"用户要求转人工\"\n        if emotion.emotion.value == \"angry\" and emotion.intensity > 0.7:\n            return \"用户情绪愤怒\"\n        if emotion.urgency:\n            return \"检测到紧急情况\"\n        neg = [t for t in ctx.turns[-4:]\n               if t.emotion in (\"negative\", \"angry\")]\n        if len(neg) >= 3:\n            return \"连续多轮用户不满意\"\n        return None\n    \n    def _esc_msg(self, emotion) -> str:\n        if emotion.emotion.value == \"angry\":\n            return \"非常抱歉！立即为您转接高级客服代表，请稍候。\"\n        if emotion.urgency:\n            return \"理解您的急切，正在优先转接人工客服。\"\n        return \"好的，正在为您转接人工客服，请稍候。\"\n    \n    async def _route(self, intent, emotion, ctx) -> str:\n        handlers = {\n            \"greeting\": self._greet,\n            \"faq\": self._faq,\n            \"product\": self._product,\n            \"order\": self._order,\n            \"complaint\": self._complaint,\n            \"refund\": self._refund,\n            \"technical\": self._technical,\n            \"account\": self._account,\n        }\n        h = handlers.get(intent.intent.value, self._unknown)\n        return await h(intent, emotion, ctx)\n    \n    async def _greet(self, intent, emotion, ctx) -> str:\n        import random\n        return random.choice([\n            \"您好！很高兴为您服务，请问有什么可以帮到您的？\",\n            \"您好！我是智能客服小助手，有什么问题都可以问我~\",\n        ])\n    \n    async def _faq(self, intent, emotion, ctx) -> str:\n        sr = await self._knowledge.search(intent.raw_text)\n        return await self._knowledge.generate_answer(intent.raw_text, sr)\n    \n    async def _product(self, intent, emotion, ctx) -> str:\n        q = f\"{ctx.entities.get('product_name','')} {intent.raw_text}\"\n        sr = await self._knowledge.search(q, category=\"product\")\n        return await self._knowledge.generate_answer(q, sr)\n    \n    async def _order(self, intent, emotion, ctx) -> str:\n        oid = ctx.entities.get(\"order_id\")\n        if not oid:\n            return \"好的，我来帮您查询。请问您的订单号是多少？\"\n        return (f\"已找到订单 **{oid}**：\\n\"\n                f\"📦 商品：无线蓝牙耳机 Pro\\n\"\n                f\"💰 金额：¥299.00\\n\"\n                f\"🚚 状态：已发货，预计12-20送达\\n\"\n                f\"📍 当前：北京分拣中心\")\n    \n    async def _complaint(self, intent, emotion, ctx) -> str:\n        reason = ctx.entities.get(\"reason\", \"未明确\")\n        r = (f\"非常抱歉因「{reason}」给您带来不好的体验。\\n\\n\"\n             f\"为了更好地帮助您：\\n\"\n             f\"1. 请提供订单号\\n\"\n             f\"2. 您希望如何处理？（退款/换货/补偿）\\n\"\n             f\"我可以为您创建投诉工单，24小时内跟进。\")\n        if emotion.emotion.value in (\"angry\", \"negative\"):\n            r += \"\\n\\n也可以立即转接人工客服。\"\n        return r\n    \n    async def _refund(self, intent, emotion, ctx) -> str:\n        name = ctx.entities.get(\"product_name\", \"该商品\")\n        return (f\"好的，帮您处理{name}的退款申请。\\n\\n\"\n                \"请确认：\\n\"\n                \"1. 是否在7天退换期内？\\n\"\n                \"2. 商品是否完好？\\n\"\n                \"3. 是否有购买凭证？\\n\\n\"\n                \"满足以上条件请提供订单号，立即为您创建退款工单。\\n\"\n                \"💡 退款一般3-5个工作日原路返回。\")\n    \n    async def _technical(self, intent, emotion, ctx) -> str:\n        sr = await self._knowledge.search(intent.raw_text, category=\"tech\")\n        if sr.has_answer:\n            ans = await self._knowledge.generate_answer(intent.raw_text, sr)\n            return ans + \"\\n\\n如无法解决，请提供设备型号和系统版本。\"\n        return (\"了解您遇到了技术问题，请提供：\\n\"\n                \"1. 设备型号和系统版本\\n\"\n                \"2. App版本号\\n\"\n                \"3. 问题出现的时间和频率\\n\"\n                \"4. 错误提示截图或文字\")\n    \n    async def _account(self, intent, emotion, ctx) -> str:\n        return (\"好的，常见账户问题处理：\\n\\n\"\n                \"**忘记密码**：登录页→忘记密码→手机验证码重置\\n\"\n                \"**手机号更换**：请提供新旧手机号，创建工单处理\\n\"\n                \"**账号被盗**：请立即联系人工客服冻结账号\\n\\n\"\n                \"请告诉我您具体遇到了什么问题？\")\n    \n    async def _unknown(self, intent, emotion, ctx) -> str:\n        if intent.confidence < 0.5:\n            return (\"抱歉，我可能没有完全理解。能否换个方式描述？\\n\"\n                    \"比如：\\n- 咨询的产品或服务\\n\"\n                    \"- 遇到的具体问题\\n- 订单号（如适用）\")\n        sr = await self._knowledge.search(intent.raw_text)\n        if sr.has_answer:\n            return await self._knowledge.generate_answer(intent.raw_text, sr)\n        return \"感谢咨询。建议转接人工客服以获得更精准的帮助。\"",
      "section_ref": "26.3.5",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-9",
      "language": "python",
      "description": "",
      "code": "# app/models/ticket.py\n\"\"\"工单数据模型\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom enum import Enum\nimport uuid\n\n\nclass TicketStatus(str, Enum):\n    OPEN = \"open\"\n    IN_PROGRESS = \"in_progress\"\n    RESOLVED = \"resolved\"\n    CLOSED = \"closed\"\n\n\nclass TicketPriority(str, Enum):\n    LOW = \"low\"\n    MEDIUM = \"medium\"\n    HIGH = \"high\"\n    URGENT = \"urgent\"\n\n\n@dataclass\nclass Ticket:\n    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])\n    user_id: str = \"\"\n    session_id: str = \"\"\n    title: str = \"\"\n    description: str = \"\"\n    category: str = \"other\"\n    priority: TicketPriority = TicketPriority.MEDIUM\n    status: TicketStatus = TicketStatus.OPEN\n    assigned_agent: str | None = None\n    created_at: datetime = field(default_factory=datetime.now)\n    dialog_summary: str = \"\"\n    tags: list = field(default_factory=list)",
      "section_ref": "26.3.6",
      "runnable": true,
      "dependencies": []
    },
    {
      "id": "code-10",
      "language": "python",
      "description": "tags: list = field(defaultfactory=list)",
      "code": "# app/services/ticket_service.py\n\"\"\"工单服务\"\"\"\n\nfrom typing import List, Optional\nfrom app.models.ticket import Ticket, TicketStatus, TicketPriority\n\n\nclass TicketService:\n    def __init__(self):\n        self._tickets: dict[str, Ticket] = {}\n    \n    async def create_ticket(\n        self, user_id: str, session_id: str, title: str,\n        description: str, category: str = \"other\",\n        priority: str = \"medium\", dialog_summary: str = \"\",\n    ) -> Ticket:\n        ticket = Ticket(\n            user_id=user_id, session_id=session_id,\n            title=title, description=description,\n            category=category,\n            priority=TicketPriority(priority),\n            dialog_summary=dialog_summary,\n        )\n        self._tickets[ticket.id] = ticket\n        return ticket\n    \n    async def auto_create(self, context, reason: str = \"\") -> Ticket:\n        \"\"\"基于对话上下文自动创建工单\"\"\"\n        priority = \"high\" if context.current_emotion == \"angry\" else \"medium\"\n        cat_map = {\n            \"refund\": \"refund\", \"complaint\": \"complaint\",\n            \"technical\": \"technical\", \"account\": \"account\",\n        }\n        category = cat_map.get(context.current_intent, \"other\")\n        \n        return await self.create_ticket(\n            user_id=context.user_id,\n            session_id=context.session_id,\n            title=f\"[{category}] {context.current_intent} - {reason}\",\n            description=context.get_history_text(10),\n            category=category,\n            priority=priority,\n        )\n    \n    async def get_ticket(self, tid: str) -> Optional[Ticket]:\n        return self._tickets.get(tid)\n    \n    async def update_status(self, tid: str, status: TicketStatus):\n        t = self._tickets.get(tid)\n        if t:\n            t.status = status\n    \n    async def list_tickets(\n        self, user_id: Optional[str] = None,\n        status: Optional[TicketStatus] = None,\n    ) -> List[Ticket]:\n        tickets = list(self._tickets.values())\n        if user_id:\n            tickets = [t for t in tickets if t.user_id == user_id]\n        if status:\n            tickets = [t for t in tickets if t.status == status]\n        return sorted(tickets, key=lambda t: t.created_at, reverse=True)",
      "section_ref": "26.3.6",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-11",
      "language": "python",
      "description": "",
      "code": "# app/services/human_handoff.py\n\"\"\"人机协作管理\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import Optional, Dict, List\nimport uuid\n\n\n@dataclass\nclass HumanAgent:\n    agent_id: str\n    name: str\n    skills: List[str] = field(default_factory=list)\n    is_online: bool = True\n    max_sessions: int = 5\n    current_sessions: int = 0\n    \n    @property\n    def available(self) -> bool:\n        return self.is_online and self.current_sessions < self.max_sessions\n\n\n@dataclass\nclass HandoffRequest:\n    request_id: str\n    session_id: str\n    user_id: str\n    reason: str\n    priority: int = 0\n    status: str = \"queuing\"  # queuing|connected|completed\n    assigned_agent: Optional[str] = None\n    created_at: datetime = field(default_factory=datetime.now)\n\n\nclass HumanHandoffManager:\n    def __init__(self):\n        self._agents: Dict[str, HumanAgent] = {}\n        self._queue: List[HandoffRequest] = []\n        self._active: Dict[str, HandoffRequest] = {}\n    \n    def register_agent(self, agent: HumanAgent):\n        self._agents[agent.agent_id] = agent\n    \n    async def request_handoff(\n        self, session_id: str, user_id: str, reason: str,\n        priority: int = 0, dialog_context: dict = None,\n    ) -> HandoffRequest:\n        req = HandoffRequest(\n            request_id=str(uuid.uuid4())[:8],\n            session_id=session_id, user_id=user_id,\n            reason=reason, priority=priority,\n        )\n        \n        agent = self._find_agent(dialog_context or {})\n        if agent:\n            return self._assign(req, agent)\n        \n        self._queue.append(req)\n        self._queue.sort(key=lambda r: r.priority, reverse=True)\n        return req\n    \n    def _find_agent(self, ctx: dict) -> Optional[HumanAgent]:\n        \"\"\"寻找合适的空闲坐席：优先技能匹配，再选最空闲的\"\"\"\n        available = [a for a in self._agents.values() if a.available]\n        if not available:\n            return None\n        intent = ctx.get(\"current_intent\", \"\")\n        for a in available:\n            if intent in a.skills:\n                return a\n        return min(available, key=lambda a: a.current_sessions)\n    \n    def _assign(self, req: HandoffRequest, agent: HumanAgent):\n        req.status = \"connected\"\n        req.assigned_agent = agent.agent_id\n        agent.current_sessions += 1\n        self._active[req.request_id] = req\n        return req\n    \n    async def complete_handoff(self, req_id: str):\n        req = self._active.pop(req_id, None)\n        if req and req.assigned_agent:\n            agent = self._agents.get(req.assigned_agent)\n            if agent:\n                agent.current_sessions -= 1\n        # 自动分配排队中的请求\n        if self._queue:\n            next_req = self._queue.pop(0)\n            agent = self._find_agent({})\n            if agent:\n                self._assign(next_req, agent)\n    \n    def get_queue_status(self) -> dict:\n        return {\n            \"queue_length\": len(self._queue),\n            \"active_handoffs\": len(self._active),\n            \"available_agents\": sum(\n                1 for a in self._agents.values() if a.available),\n        }",
      "section_ref": "26.3.7",
      "runnable": true,
      "dependencies": []
    },
    {
      "id": "code-12",
      "language": "python",
      "description": "",
      "code": "# app/main.py\n\"\"\"智能客服系统 - FastAPI 入口\"\"\"\n\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI, WebSocket, WebSocketDisconnect\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom pydantic import BaseModel\nfrom typing import Optional, List\nimport json, uuid\n\nfrom app.config import settings\nfrom app.agents.dialog_agent import DialogAgent, DialogContext\nfrom app.agents.knowledge_agent import KnowledgeAgent\nfrom app.services.ticket_service import TicketService\nfrom app.services.human_handoff import HumanHandoffManager, HumanAgent\n\n\ndialog_agent = DialogAgent()\nknowledge_agent = KnowledgeAgent()\nticket_service = TicketService()\nhandoff_manager = HumanHandoffManager()\nsessions: dict[str, DialogContext] = {}\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    # 初始化坐席\n    handoff_manager.register_agent(HumanAgent(\n        \"agent_001\", \"张主管\", [\"complaint\", \"refund\"]))\n    handoff_manager.register_agent(HumanAgent(\n        \"agent_002\", \"李工程师\", [\"technical\", \"product\"]))\n    handoff_manager.register_agent(HumanAgent(\n        \"agent_003\", \"王客服\", [\"faq\", \"order\", \"account\"],\n        max_sessions=8))\n    _init_kb()\n    print(f\"🚀 {settings.APP_NAME} v{settings.APP_VERSION} 启动完成\")\n    yield\n\n\napp = FastAPI(title=settings.APP_NAME,\n              version=settings.APP_VERSION, lifespan=lifespan)\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n                   allow_credentials=True,\n                   allow_methods=[\"*\"], allow_headers=[\"*\"])\n\n\nclass ChatRequest(BaseModel):\n    user_id: str\n    message: str\n    session_id: Optional[str] = None\n\n\n@app.get(\"/health\")\nasync def health():\n    return {\"status\": \"ok\", \"version\": settings.APP_VERSION}\n\n\n@app.post(\"/api/v1/chat\")\nasync def chat(req: ChatRequest):\n    \"\"\"发送消息并获取回复\"\"\"\n    sid = req.session_id or str(uuid.uuid4())[:8]\n    if sid not in sessions:\n        sessions[sid] = DialogContext(session_id=sid, user_id=req.user_id)\n    \n    ctx = sessions[sid]\n    resp = await dialog_agent.process(req.message, ctx)\n    \n    ticket_id = None\n    if resp.should_escalate:\n        ho = await handoff_manager.request_handoff(\n            session_id=sid, user_id=req.user_id,\n            reason=resp.escalate_reason,\n            priority=10 if resp.emotion == \"angry\" else 5,\n            dialog_context={\n                \"current_intent\": ctx.current_intent,\n                \"current_emotion\": ctx.current_emotion,\n            },\n        )\n        ticket = await ticket_service.auto_create(\n            ctx, reason=resp.escalate_reason)\n        ticket_id = ticket.id\n        \n        if ho.status == \"connected\":\n            resp.content += \"\\n\\n✅ 已转接人工客服，请稍候。\"\n        else:\n            resp.content += \"\\n\\n📋 正在排队等待，请耐心等待。\"\n    \n    return {\n        \"session_id\": sid, \"reply\": resp.content,\n        \"intent\": resp.intent, \"emotion\": resp.emotion,\n        \"confidence\": resp.confidence,\n        \"should_escalate\": resp.should_escalate,\n        \"ticket_id\": ticket_id,\n    }\n\n\n@app.post(\"/api/v1/knowledge\")\nasync def add_knowledge(\n    question: str, answer: str,\n    category: str = \"general\",\n    tags: Optional[List[str]] = None,\n):\n    doc_id = knowledge_agent.add_knowledge(\n        question=question, answer=answer,\n        category=category, tags=tags)\n    return {\"id\": doc_id, \"status\": \"created\"}\n\n\n@app.websocket(\"/ws/chat/{sid}\")\nasync def ws_chat(ws: WebSocket, sid: str):\n    \"\"\"WebSocket 实时聊天\"\"\"\n    await ws.accept()\n    try:\n        uid = None\n        while True:\n            data = json.loads(await ws.receive_text())\n            if not uid:\n                uid = data.get(\"user_id\", \"anon\")\n            if sid not in sessions:\n                sessions[sid] = DialogContext(session_id=sid, user_id=uid)\n            resp = await dialog_agent.process(data[\"message\"], sessions[sid])\n            await ws.send_json({\n                \"reply\": resp.content, \"intent\": resp.intent,\n                \"emotion\": resp.emotion,\n                \"should_escalate\": resp.should_escalate,\n            })\n    except WebSocketDisconnect:\n        pass\n\n\ndef _init_kb():\n    \"\"\"初始化知识库\"\"\"\n    items = [\n        (\"你们支持哪些支付方式？\",\n         \"支持：微信支付、支付宝、银联卡、花呗分期、京东白条。即时到账。\",\n         \"faq\", [\"支付\", \"付款\"]),\n        (\"退货政策是什么？\",\n         \"7天无理由退换。15天质量问题包换。30天包修。流程：我的订单→申请退货→审核→寄回→3-5天退款。\",\n         \"faq\", [\"退货\", \"退款\"]),\n        (\"App闪退怎么解决？\",\n         \"1. 更新App 2. 清除缓存 3. 重启手机 4. 重装App。如仍无法解决，提供设备型号。\",\n         \"technical\", [\"闪退\", \"崩溃\"]),\n        (\"无线蓝牙耳机Pro有什么特点？\",\n         \"40mm驱动单元，主动降噪35dB，蓝牙5.3，续航36小时，快充10分=2小时，IPX5防水。\",\n         \"product\", [\"耳机\"]),\n    ]\n    for q, a, cat, tags in items:\n        knowledge_agent.add_knowledge(q, a, cat, tags)\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n    uvicorn.run(\"app.main:app\", host=\"0.0.0.0\", port=8000,\n                reload=settings.DEBUG)",
      "section_ref": "26.3.8",
      "runnable": true,
      "dependencies": [
        "contextlib",
        "fastapi",
        "pydantic",
        "app"
      ]
    },
    {
      "id": "code-13",
      "language": "python",
      "description": "",
      "code": "# tests/test_intent.py\n\"\"\"意图识别测试\"\"\"\n\nimport pytest\nfrom app.agents.intent_agent import IntentAgent, IntentType\n\n\n@pytest.fixture\ndef intent_agent():\n    return IntentAgent()\n\n\nclass TestIntentAgent:\n    \n    @pytest.mark.asyncio\n    async def test_faq_intent(self, intent_agent):\n        result = await intent_agent.classify(\"你们支持什么支付方式？\")\n        assert result.intent == IntentType.FAQ\n        assert result.confidence > 0.5\n    \n    @pytest.mark.asyncio\n    async def test_order_intent(self, intent_agent):\n        result = await intent_agent.classify(\n            \"我的订单 ORD-2024-1234 什么时候发货？\")\n        assert result.intent == IntentType.ORDER_QUERY\n        assert \"order_id\" in result.entities\n    \n    @pytest.mark.asyncio\n    async def test_complaint_intent(self, intent_agent):\n        result = await intent_agent.classify(\n            \"我要投诉，你们的快递太慢了！\")\n        assert result.intent == IntentType.COMPLAINT\n        assert result.confidence > 0.7\n    \n    @pytest.mark.asyncio\n    async def test_transfer_intent(self, intent_agent):\n        result = await intent_agent.classify(\"转人工客服！\")\n        assert result.intent == IntentType.TRANSFER_HUMAN\n    \n    @pytest.mark.asyncio\n    async def test_fallback(self, intent_agent):\n        result = intent_agent._fallback(\"我要退货退款\")\n        assert result.intent == IntentType.REFUND",
      "section_ref": "26.4.1",
      "runnable": true,
      "dependencies": [
        "pytest",
        "app"
      ]
    },
    {
      "id": "code-14",
      "language": "python",
      "description": "",
      "code": "# tests/test_dialog.py\n\"\"\"对话管理测试\"\"\"\n\nimport pytest\nfrom app.agents.dialog_agent import DialogAgent, DialogContext\n\n\n@pytest.fixture\ndef dialog_agent():\n    return DialogAgent()\n\n\n@pytest.fixture\ndef context():\n    return DialogContext(session_id=\"test\", user_id=\"test_user\")\n\n\nclass TestDialogAgent:\n    \n    @pytest.mark.asyncio\n    async def test_greeting(self, dialog_agent, context):\n        resp = await dialog_agent.process(\"你好\", context)\n        assert resp.intent == \"greeting\"\n        assert not resp.should_escalate\n    \n    @pytest.mark.asyncio\n    async def test_multi_turn(self, dialog_agent, context):\n        r1 = await dialog_agent.process(\"你好\", context)\n        assert context.turn_count == 1\n        r2 = await dialog_agent.process(\n            \"你们支持什么支付方式？\", context)\n        assert context.turn_count == 2\n        assert r2.intent == \"faq\"\n    \n    @pytest.mark.asyncio\n    async def test_escalation(self, dialog_agent, context):\n        resp = await dialog_agent.process(\"转人工！\", context)\n        assert resp.should_escalate is True",
      "section_ref": "26.4.2",
      "runnable": true,
      "dependencies": [
        "pytest",
        "app"
      ]
    },
    {
      "id": "code-15",
      "language": "python",
      "description": "",
      "code": "# tests/test_api.py\n\"\"\"API 集成测试\"\"\"\n\nimport pytest\nfrom httpx import AsyncClient, ASGITransport\nfrom app.main import app\n\n\n@pytest.mark.anyio\nasync def test_health():\n    async with AsyncClient(\n        transport=ASGITransport(app=app),\n        base_url=\"http://test\"\n    ) as client:\n        r = await client.get(\"/health\")\n        assert r.status_code == 200\n\n\n@pytest.mark.anyio\nasync def test_chat():\n    async with AsyncClient(\n        transport=ASGITransport(app=app),\n        base_url=\"http://test\"\n    ) as client:\n        r = await client.post(\"/api/v1/chat\", json={\n            \"user_id\": \"test\", \"message\": \"你好\"})\n        assert r.status_code == 200\n        data = r.json()\n        assert data[\"intent\"] == \"greeting\"\n        assert \"session_id\" in data",
      "section_ref": "26.4.3",
      "runnable": true,
      "dependencies": [
        "pytest",
        "httpx",
        "app"
      ]
    },
    {
      "id": "code-16",
      "language": "dockerfile",
      "description": "",
      "code": "FROM python:3.11-slim\nWORKDIR /app\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    build-essential && rm -rf /var/lib/apt/lists/*\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY app/ ./app/\nEXPOSE 8000\n\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]",
      "section_ref": "26.5.1",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-17",
      "language": "yaml",
      "description": "CMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]",
      "code": "# docker-compose.yml\nversion: '3.8'\nservices:\n  customer-service:\n    build: .\n    ports:\n      - \"8000:8000\"\n    environment:\n      - CS_LLM_API_KEY=${LLM_API_KEY}\n      - CS_REDIS_URL=redis://redis:6379/0\n      - CS_DATABASE_URL=postgresql://cs:cs123@postgres:5432/cs\n    depends_on:\n      - redis\n      - postgres\n\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"6379:6379\"\n\n  postgres:\n    image: postgres:15-alpine\n    environment:\n      POSTGRES_DB: cs\n      POSTGRES_USER: cs\n      POSTGRES_PASSWORD: cs123\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n\nvolumes:\n  pgdata:",
      "section_ref": "26.5.1",
      "runnable": false,
      "dependencies": []
    }
  ],
  "tables": [
    {
      "headers": [
        "维度",
        "指标"
      ],
      "data": [
        [
          "响应时间",
          "P95 < 2 秒"
        ],
        [
          "并发支持",
          "500 QPS"
        ],
        [
          "可用性",
          "99.9%"
        ],
        [
          "知识库容量",
          "10 万+ FAQ 条目"
        ],
        [
          "多轮对话深度",
          "最多 20 轮"
        ]
      ]
    },
    {
      "headers": [
        "模式",
        "应用场景",
        "效果"
      ],
      "data": [
        [
          "优雅降级",
          "LLM 不可用时回退到规则匹配",
          "系统可用性从 99% 提升到 99.9%"
        ],
        [
          "多信号融合",
          "转人工决策",
          "减少误判 60%"
        ],
        [
          "技能路由",
          "坐席分配",
          "客户满意度提升 25%"
        ],
        [
          "知识增强生成",
          "FAQ 回答",
          "回答准确率 92% → 96%"
        ]
      ]
    }
  ],
  "key_takeaways": [],
  "common_pitfalls": [],
  "related_chapters": [
    "ch06",
    "ch07",
    "ch08"
  ]
}