全书主线进度 :第 2 章搭好了生产级工程底座。现在,我们开始第一个实战项目。这是全书最简单的项目——只有一个 Agent、三个工具、一个精心设计的 Prompt ——但它完整覆盖了做一款 AI Agent 产品从需求到上线的全链路。更重要的是,它让你第一次亲眼看到:一个只会"说话"的模型,是如何通过 Tool 真正"做事"的。
主讲能力 :Tool(工具调用)+ Prompt(提示词工程)
业务场景 :「云购商城」电商客服,自动查订单、查物流、查售后政策,超范围转人工。
技术栈 :LangChain v1.0 create_agent + 三个自定义 Tool + 精心设计的 System Prompt + SQLite 持久化
开篇:客服小张的崩溃一天
凌晨一点,「云购商城」的客服小张还在回消息。
「我那个订单发货了吗?」「10001 到哪了?」「能退货吗?」……同一个问题,换着花样问,一晚上几百条。小张不是不敬业,是这种活儿太磨人——答案就在系统里摆着,他只是个人肉查询接口。他盯着屏幕想:这些破事,就不能让机器干吗?
第二天,技术部给了他一个「智能客服 Agent」。小张将信将疑地让它上线,自己躲去喝茶。结果一整个下午,买家查订单、查物流、问退换货,Agent 全接住了,回答又准又快。只有遇到「我要投诉你们发货太慢」这种事,它才老老实实把人转给小张。
小张有点恍惚:它又没长眼睛,怎么知道订单发了没?它又没去过仓库,怎么知道包裹到哪了?
答案就藏在两个词里:Tool(工具) 和 Prompt(提示词) 。这一章,我们就把小张的疑问彻底解开——亲手造一个这样的客服 Agent。它会是我们「培养新员工」之路上的第一站:教他学会用工具,并学会好好说话 。
3.1 小张的诉求
带着小张的烦恼,我们正式进入需求分析。一个能替小张挡掉 70% 重复问题的客服 Agent,到底要满足什么?
3.1.1 业务背景与痛点
一家中型电商平台的客服团队每天要处理上千条重复咨询,其中 70% 以上属于三类固定问题:
查订单 :「我的订单发货了吗?」「订单金额是多少?」
查物流 :「到哪了?」「什么时候送到?」
查政策 :「能退吗?」「运费谁出?」
人工客服反复回答这些问题,效率低、成本高、情绪消耗大。而这恰好是 AI Agent 最擅长的场景——标准化、高频、有明确数据源的问题,交给 Agent 自动处理 。
3.1.2 目标用户与画像
用户角色
特征
典型需求
普通买家
手机端居多,对技术不敏感
查订单状态、查物流、问退换货
长辈用户
不太会打字,习惯自然语言提问
口语化表达,如"我那耳机发了吗"
3.1.3 用户故事
编号
作为
我想要
以便
US-1
买家
输入订单号,立刻知道订单状态
不用打电话或等人工
US-2
买家
输入订单号,知道包裹到哪了
安排收货
US-3
买家
询问退换货条件
决定要不要退
US-4
买家
超出能力的问题转人工
复杂问题不被耽误
US-5
运营
客服机器人 7×24 在线
降低人工成本
US-6
运维
服务有健康检查、指标、日志
生产环境可运维
3.1.4 功能性需求
FR-1 订单查询 :用户说"订单 XXX"或"查订单",Agent 调用 query_order 返回状态、商品、金额
FR-2 物流查询 :用户说"到哪了"“物流”,Agent 调用 query_logistics 返回配送进度
FR-3 政策查询 :用户问"退换货"“运费谁出”,Agent 调用 query_policy 返回政策原文
FR-4 转人工 :涉及退款审批、投诉、无法识别的问题,Agent 礼貌引导转人工
FR-5 友好交互 :语气亲切但不啰嗦,先共情再给方案
FR-6 生产级可靠性 :数据库故障时工具返回降级提示,不崩溃;模型调用失败时自动重试后返回兜底文案
3.1.5 非功能性需求
单次响应 < 5 秒(含工具调用)
回答必须基于工具返回的真实数据,不得编造
订单号、金额等敏感信息原文展示,不篡改
数据库故障时优雅降级,返回"服务暂时不可用"
支持多品牌(通过配置切换 System Prompt 中的品牌名)
3.1.6 验收标准
场景
输入
期望输出
正常查订单
“帮我查订单 10001”
返回无线耳机、299 元、已发货
不存在的订单
“查订单 99999”
提示未找到,请核对订单号
查物流
“10001 到哪了”
返回杭州转运中心,预计明天送达
问政策
“能退货吗”
返回 7 天无理由退货政策
超范围
“我要投诉”
礼貌引导转人工
数据库故障
模拟数据库异常
返回"服务暂时不可用",不崩溃
3.2 画个样子:它该长啥样
3.2.1 功能架构图
%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph TD
U["用户输入"] --> Agent["智能客服 Agent create_agent"]
Agent --> T1["🔧 query_order 查订单 → SQLite orders 表"]
Agent --> T2["🔧 query_logistics 查物流 → SQLite logistics 表"]
Agent --> T3["🔧 query_policy 查政策 → SQLite policies 表"]
Agent --> Fallback["转人工回复"]
T1 & T2 & T3 --> Reply["返回最终回复"]
Fallback --> Reply
classDef uN fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
classDef aN fill:#ede7f6,stroke:#5e35b1,stroke-width:2px,color:#311b92
classDef tN fill:#e0f7fa,stroke:#00acc1,stroke-width:2px,color:#006064
classDef rN fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#1b5e20
class U uN
class Agent aN
class T1,T2,T3 tN
class Fallback,Reply rN
3.2.2 核心交互流程
%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
sequenceDiagram
participant U as 用户
participant A as Agent
participant T as 工具
participant DB as SQLite
U->>A: "帮我查订单 10001"
Note over A: 思考:需要查订单
A->>T: query_order("10001")
T->>DB: SELECT * FROM orders WHERE id='10001'
DB->>T: {无线耳机, 299元, 已发货}
T->>A: 订单 10001:无线耳机...
Note over A: 组织语言回复
A->>U: "您的订单 10001(无线耳机,299元)已发货!"
3.2.3 边界与异常处理
异常
处理方式
订单号不存在
工具返回"未找到",Agent 转述给用户
物流暂无信息
提示"可能尚未发货"
用户输入无法识别
Agent 礼貌说明能力范围,引导转人工
数据库故障
工具捕获异常,返回"服务暂时不可用"
模型 API 超时
自动重试 3 次(指数退避),最终失败返回兜底文案
瞬时高并发
流控检查,超出阈值返回"系统繁忙"
3.3 拆开看:怎么造出来
3.3.1 系统架构图
%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph TD
U["用户"] --> FE["前端管理台"]
FE --> API["FastAPI /stream"]
API --> P01["项目一 Agent"]
P01 --> Core["BaseProject.run() 流控 + 重试 + 指标"]
Core --> LC["create_agent (LangChain v1.0)"]
LC --> Model["Claude Sonnet 4.6"]
LC --> T1["query_order"]
LC --> T2["query_logistics"]
LC --> T3["query_policy"]
T1 & T2 & T3 --> DB["SQLite customer_service.db"]
classDef uN fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
classDef sN fill:#ede7f6,stroke:#5e35b1,stroke-width:2px,color:#311b92
classDef aN fill:#e0f2f1,stroke:#00897b,stroke-width:2px,color:#004d40
classDef tN fill:#e0f7fa,stroke:#00acc1,stroke-width:2px,color:#006064
class U uN
class FE,API,Core sN
class P01,LC,Model aN
class T1,T2,T3,DB tN
3.3.2 技术选型与理由
技术点
选型
理由
Agent 框架
langchain.agents.create_agent
官方 v1.0 推荐;内置 agent loop
大模型
Claude Sonnet 4.6
长上下文、经济实惠、中文流畅
工具定义
@tool 装饰器
官方方式,docstring 自动转为工具说明书
数据存储
SQLite (可替换为 PostgreSQL)
零配置、零运维,适合学习和单机部署
错误处理
工具级 try/except + 基类级重试
两级兜底,故障不崩溃
后端
FastAPI(复用第 2 章)
统一入口,支持 SSE 流式
前端
React(复用第 2 章)
统一管理台
3.3.3 数据库设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CREATE TABLE IF NOT EXISTS orders ( id TEXT PRIMARY KEY , item TEXT NOT NULL , amount REAL NOT NULL , status TEXT NOT NULL DEFAULT '待付款' , created_at TEXT NOT NULL DEFAULT (datetime('now' )) ); CREATE TABLE IF NOT EXISTS logistics ( order_id TEXT PRIMARY KEY , tracking_info TEXT NOT NULL , updated_at TEXT NOT NULL DEFAULT (datetime('now' )), FOREIGN KEY (order_id) REFERENCES orders(id) ); CREATE TABLE IF NOT EXISTS policies ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL , content TEXT NOT NULL );
设计要点 :
三张表各自独立,通过 order_id 外键关联
种子数据在模块加载时自动插入(幂等:仅当表为空时)
生产环境可替换为 PostgreSQL,只需改连接字符串
3.3.4 关键设计:工具的错误处理
每个工具函数都用 try/except/finally 包裹,捕获 sqlite3.Error 后返回用户友好的降级文案,而非抛出异常堆栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @tool def query_order (order_id: str ) -> str : """根据订单号查询订单状态、商品和金额。""" conn = None try : conn = _get_db() row = conn.execute("SELECT ..." ).fetchone() if not row: return f"未找到订单 {order_id} ,请核对订单号后重试。" return f"订单 {order_id} :{row['item' ]} ..." except sqlite3.Error as e: logger.error("查询订单失败: order_id=%s error=%s" , order_id, e) return "订单查询服务暂时不可用,请稍后重试或联系人工客服。" finally : if conn: conn.close()
三层错误处理 :
工具层 (这里):数据库异常返回降级文案
基类层 (BaseProject.run()):模型调用失败自动重试
最终兜底 :重试全部失败后返回"抱歉,处理您的请求时遇到了技术问题"
3.3.5 关键设计:Prompt 工程
System Prompt 的设计原则:
角色设定 :第一句明确身份——“你是「云购商城」的金牌智能客服”
工具使用约束 :明确要求"涉及订单、物流、售后政策的问题,必须调用相应工具,不得凭空回答"
语气风格 :“热情、专业、简洁”+“先共情再给方案”
边界设定 :“超出能力范围礼貌转人工”
错误传播 :“如果工具返回错误,如实告知用户并建议稍后重试”
3.4 动手写:三层架构完整代码
3.4.1 三层架构总览
项目一采用经典的三层架构设计,关注点分离,便于维护和扩展:
层级
职责
对应文件
依赖
领域模型层
定义业务实体的数据结构
models.py
无(纯数据类)
数据仓储层
封装数据库操作,返回领域模型
repositories.py
models
工具服务层
Agent 可调用的工具,带错误降级
tools.py
repositories
Prompt 层
配置驱动的 System Prompt 生成
prompts.py
无
Agent 工厂层
统一构建 Agent 实例
service.py
tools, prompts
项目定义层
继承 BaseProject,注册到全局路由
project.py
service
入口层
重新导出对外接口,注册项目
__init__.py
project, tools
架构优势 :
可测试性 :每层都可以单独单元测试
可替换性 :SQLite 仓储可无缝替换为 PostgreSQL 仓储
可扩展性 :新增工具只需在 tools.py 添加,不影响其他层
向后兼容 :from projects.p01_customer_service import query_order 继续可用
3.4.2 三层架构完整代码
代码清单 3-1:领域模型层(models.py )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 """领域模型层:定义订单、物流、政策等核心数据结构。""" from __future__ import annotationsfrom dataclasses import dataclassfrom enum import Enumclass OrderStatus (str , Enum): """订单状态枚举。""" PENDING_PAYMENT = "待付款" PAID = "已付款" SHIPPED = "已发货" DELIVERED = "已签收" CANCELLED = "已取消" @dataclass class Order : """订单领域模型。""" id : str item: str amount: float status: OrderStatus created_at: str def __str__ (self ) -> str : return f"订单 {self.id } :{self.item} ,金额 {self.amount} 元,状态:{self.status} 。" @dataclass class LogisticsInfo : """物流信息领域模型。""" order_id: str tracking_info: str updated_at: str def __str__ (self ) -> str : return f"订单 {self.order_id} 物流:{self.tracking_info} " @dataclass class Policy : """售后政策领域模型。""" id : int topic: str content: str def __str__ (self ) -> str : return f"【{self.topic} 】{self.content} "
设计要点 :
使用 str, Enum 双重继承,既保证类型安全,又能直接和数据库字符串比较
枚举值使用中文,是展示友好设计,便于直接输出给用户
生产环境多语言方案:可改为 code + display_name 结构,如 PENDING_PAYMENT = ("pending_payment", "待付款"),根据用户语言选择展示文本
每个模型实现 __str__(),工具层直接 str(order) 输出给用户
不依赖任何外部库,纯数据类
代码清单 3-2:数据仓储层(repositories.py )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 """数据仓储层:SQLite 持久化存储。 这是三层架构的中间层,负责数据访问。 封装数据库操作,向上层提供领域模型对象。 > 设计说明: > - SQLite 适合教学演示和单机部署,零配置零运维 > - 生产环境可替换为 PostgreSQL,只需修改连接字符串和驱动 > - 仓储层接口保持不变,上层代码无需修改 """ from __future__ import annotationsimport sqlite3from functools import lru_cachefrom pathlib import Pathfrom typing import List , Optional from core.config import DATA_DIRfrom core.logging_conf import get_loggerfrom .models import LogisticsInfo, Order, OrderStatus, Policylogger = get_logger("p01.repository" ) DB_PATH = DATA_DIR / "customer_service.db" class CustomerServiceRepository : """客服数据仓储,封装所有数据库操作。 所有方法均返回领域模型对象或 None,上层无需处理原始 Row。 """ def __init__ (self, db_path: Path = DB_PATH ) -> None : self ._db_path = db_path self ._init_db() def _get_db (self ) -> sqlite3.Connection: """获取数据库连接(每次新建,避免跨线程共享问题)。""" conn = sqlite3.connect(str (self ._db_path)) conn.row_factory = sqlite3.Row return conn def _init_db (self ) -> None : """初始化数据库表与种子数据(幂等)。""" conn = self ._get_db() conn.executescript(""" CREATE TABLE IF NOT EXISTS orders ( id TEXT PRIMARY KEY, item TEXT NOT NULL, amount REAL NOT NULL, status TEXT NOT NULL DEFAULT '待付款', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS logistics ( order_id TEXT PRIMARY KEY, tracking_info TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (order_id) REFERENCES orders(id) ); CREATE TABLE IF NOT EXISTS policies ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, content TEXT NOT NULL ); """ ) cur = conn.execute("SELECT COUNT(*) FROM orders" ) if cur.fetchone()[0 ] == 0 : conn.executescript(""" INSERT INTO orders(id, item, amount, status) VALUES ('10001', '无线耳机', 299, '已发货'), ('10002', '机械键盘', 459, '待付款'), ('10003', '便携充电宝', 129, '已签收'); INSERT INTO logistics(order_id, tracking_info) VALUES ('10001', '已到达【杭州转运中心】,预计明天送达'); INSERT INTO policies(topic, content) VALUES ('退换货', '商品签收后 7 天内无理由退货(商品需保持完好);' '质量问题 15 天内可换货,运费由商家承担。'), ('发票', '电子发票在签收后 3 个工作日内发送至您的注册邮箱。'); """ ) conn.commit() conn.close() def get_order (self, order_id: str ) -> Optional [Order]: """根据订单号查询订单,返回领域模型或 None。""" conn = None try : conn = self ._get_db() row = conn.execute( "SELECT id, item, amount, status, created_at FROM orders WHERE id = ?" , (order_id,), ).fetchone() if not row: return None return Order( id =row["id" ], item=row["item" ], amount=row["amount" ], status=OrderStatus(row["status" ]), created_at=row["created_at" ], ) except sqlite3.Error as e: logger.error("查询订单失败: order_id=%s error=%s" , order_id, e) raise finally : if conn: conn.close() def get_logistics (self, order_id: str ) -> Optional [LogisticsInfo]: """根据订单号查询物流信息。""" conn = None try : conn = self ._get_db() row = conn.execute( "SELECT order_id, tracking_info, updated_at FROM logistics WHERE order_id = ?" , (order_id,), ).fetchone() if not row: return None return LogisticsInfo( order_id=row["order_id" ], tracking_info=row["tracking_info" ], updated_at=row["updated_at" ], ) except sqlite3.Error as e: logger.error("查询物流失败: order_id=%s error=%s" , order_id, e) raise finally : if conn: conn.close() def search_policies (self, keyword: str ) -> List [Policy]: """根据关键词搜索售后政策。""" conn = None try : conn = self ._get_db() rows = conn.execute( "SELECT id, topic, content FROM policies " "WHERE topic LIKE ? OR content LIKE ?" , (f"%{keyword} %" , f"%{keyword} %" ), ).fetchall() return [ Policy( id =r["id" ], topic=r["topic" ], content=r["content" ], ) for r in rows ] except sqlite3.Error as e: logger.error("查询政策失败: keyword=%s error=%s" , keyword, e) raise finally : if conn: conn.close() @lru_cache(maxsize=None ) def get_repository () -> CustomerServiceRepository: """获取仓储单例(线程安全)。 使用 functools.lru_cache 实现单例模式,避免多线程环境下重复初始化。 """ return CustomerServiceRepository()
设计要点 :
仓储层只抛出异常,不做降级处理——降级是工具层的职责
使用 lru_cache 实现线程安全的单例,避免重复初始化数据库
所有方法返回领域模型对象,上层不接触原始 sqlite3.Row
代码清单 3-3:工具层(tools.py )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 """工具层:定义 Agent 可调用的工具函数。 这是三层架构的上层,依赖仓储层。 每个工具都带有错误处理和降级,保证生产稳定性。 """ from __future__ import annotationsfrom langchain.tools import toolfrom core.logging_conf import get_loggerfrom .repositories import get_repositorylogger = get_logger("p01.tools" ) @tool def query_order (order_id: str ) -> str : """根据订单号查询订单状态、商品和金额。""" try : repo = get_repository() order = repo.get_order(order_id) if not order: return f"未找到订单 {order_id} ,请核对订单号后重试。" return str (order) except Exception as e: logger.exception("查询订单失败: order_id=%s error=%s" , order_id, e) return "订单查询服务暂时不可用,请稍后重试或联系人工客服。" @tool def query_logistics (order_id: str ) -> str : """根据订单号查询物流配送进度。""" try : repo = get_repository() logistics = repo.get_logistics(order_id) if not logistics: return f"订单 {order_id} 暂无物流信息(可能尚未发货)。" return str (logistics) except Exception as e: logger.exception("查询物流失败: order_id=%s error=%s" , order_id, e) return "物流查询服务暂时不可用,请稍后重试。" @tool def query_policy (topic: str ) -> str : """查询售后政策(退货、换货、发票等)。""" try : repo = get_repository() policies = repo.search_policies(topic) if not policies: return f"未找到与「{topic} 」相关的政策,请尝试其他关键词或转人工客服。" return "\n\n" .join(str (p) for p in policies) except Exception as e: logger.exception("查询政策失败: topic=%s error=%s" , topic, e) return "政策查询服务暂时不可用,请稍后重试。"
设计要点 :
捕获 Exception(包含数据库初始化失败、IO 错误等),返回用户友好的降级文案
使用 logger.exception 自动记录完整堆栈,便于生产环境排查
业务逻辑(“未找到”、“暂无物流”)在工具层处理
工具只返回字符串,Agent 不需要理解领域模型
代码清单 3-4:Prompt 层(prompts.py )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 """Prompt 层:构建 System Prompt。""" from __future__ import annotationsdef build_system_prompt (brand: str = "云购商城" ) -> str : """按品牌生成 System Prompt。 设计原则: 1. 角色设定:第一句明确身份 2. 工具使用约束:必须调用工具,不得凭空回答 3. 语气风格:亲切、专业、简洁 4. 边界设定:超出能力礼貌转人工 5. 错误传播:工具错误如实告知用户 """ return f"""你是「{brand} 」的金牌智能客服,热情、专业、简洁。 工作要求: 1. 涉及订单、物流、售后政策的问题,必须调用相应工具获取准确信息,不得凭空回答。 2. 回答用中文,语气亲切,先共情再给方案。 3. 如果用户问题超出你的能力(如投诉、退款审批、价格谈判), 礼貌告知将转接人工客服,并说明原因。 4. 不要编造订单号、政策内容或价格信息。 5. 如果工具返回错误(如"服务不可用"),如实告知用户并建议稍后重试。"""
设计要点 :
配置驱动,支持多品牌切换
五点设计原则,覆盖 Agent 的行为边界
生产环境可扩展为从配置中心或数据库读取 Prompt 模板
代码清单 3-5:Agent 工厂层(service.py )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 """Agent 服务层:Agent 工厂。""" from __future__ import annotationsfrom langchain_core.language_models import BaseChatModelfrom langchain_core.tools import BaseToolfrom core import build_chat_model, get_settingsfrom .prompts import build_system_promptfrom .tools import query_logistics, query_order, query_policyclass CustomerServiceAgentFactory : """客服 Agent 工厂,统一构建 Agent 实例。""" @staticmethod def get_tools () -> list [BaseTool]: """获取客服可用的所有工具。""" return [query_order, query_logistics, query_policy] @staticmethod def build_agent ( model: BaseChatModel | None = None , system_prompt: str | None = None , ): """构建智能客服 Agent。""" from langchain.agents import create_agent if model is None : model = build_chat_model() if system_prompt is None : system_prompt = build_system_prompt() tools = CustomerServiceAgentFactory.get_tools() return create_agent( model=model, tools=tools, system_prompt=system_prompt, )
设计要点 :
工厂模式,统一 Agent 构建入口
支持注入自定义 model 和 system_prompt(便于测试和扩展)
工具列表集中管理,新增工具只需修改 get_tools()
代码清单 3-6:项目定义层(project.py )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 """项目定义层:Project 子类。""" from __future__ import annotationsfrom core import BaseProjectfrom .service import CustomerServiceAgentFactoryclass CustomerServiceProject (BaseProject ): """智能客服项目。 继承 BaseProject,自动获得: - 模型调用自动重试 - 流控检查 - 指标收集 - SSE 流式支持 """ id = "p01_customer_service" name = "智能客服 Agent" description = "电商客服:查订单、查物流、查售后政策,超范围转人工。" capabilities = ["Tool" , "Prompt" ] def build_agent (self ): """构建 Agent 实例。""" return CustomerServiceAgentFactory.build_agent()
设计要点 :
Project 类只做"装配",不包含业务逻辑
继承 BaseProject 自动获得生产级特性(重试、流控、指标等)
所有业务逻辑都下沉到下层
代码清单 3-7:入口层(init .py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 """项目一:智能客服 Agent(生产级)。""" from __future__ import annotationsfrom core import registryfrom .project import CustomerServiceProjectfrom .tools import query_logistics, query_order, query_policyregistry.register(CustomerServiceProject()) __all__ = [ "CustomerServiceProject" , "query_order" , "query_logistics" , "query_policy" , ]
设计要点 :
只做两件事:重新导出对外接口 + 注册项目
保证向后兼容:from projects.p01_customer_service import query_order 继续可用
模块加载时自动注册,后端通过 registry 自动发现所有项目
3.4.3 核心代码解读
在讲分层之前,先回答开篇小张的那个疑问:它又没长眼睛,怎么知道订单发了没?
这里藏着一个让初学者最绕不过去的认知关口。很多人以为「Agent 调用工具」是模型亲自去查了数据库——错。模型从头到尾只会做一件事:输出文字 。它没有手,碰不到数据库。
那它怎么「查」的?秘密在 @tool 装饰器和 create_agent 之间的一场「默契配合」:
@tool 把 query_order 这个函数的名字和 docstring (「根据订单号查询订单状态、商品和金额」)偷偷塞进给模型的输入里——相当于递给模型一本工具说明书。
模型读到用户说「查订单 10001」,又看到说明书上有「查订单」这工具,于是它不直接回答 ,而是输出一段结构化的「调用请求」:调用 query_order,参数 order_id=10001。注意,这只是它「说」要查,还没真查。
create_agent 内置的循环(Harness)接住这段请求,替 模型真正执行 query_order("10001"),拿到「无线耳机,299 元,已发货」。
循环把结果再喂回模型:「你刚要查的,结果是这个。」模型这才组织成一句人话回给用户。
所以小张看到的「它知道订单发了没」,真相是:模型表达了「我想查」的意图,程序替它查了,再把答案递回它嘴边 。模型始终没碰过数据库,但它配合程序,完成了「做事」的效果。这就是 Tool 的本质——不是让模型变成全能选手,而是让它学会「求助」。
💡 顿悟时刻 :理解了这一点,你就理解了整个 Agent 技术的根基。后面所有的 Harness、MCP、Multi-Agent,本质上都是在围绕这场「模型表达意图 ↔ 程序执行」的配合做文章——让它循环、让它标准化、让它组队。
分层的意义 :
领域模型层 是整个系统的"通用语言"——仓储层返回它,工具层把它转为字符串给 Agent。无论上层怎么变,业务实体的定义是稳定的。
仓储层 做了一个关键设计:捕获异常后只记录日志然后 raise,而不是返回降级文案。这是因为"降级"是工具层的职责——仓储层的职责是数据访问,不是决定给用户返回什么文案。单一职责原则 。
工具层 是 Agent 的"手和脚"——它知道如何处理"未找到"这种业务场景,也知道数据库挂了该给用户看什么提示。Agent 只需要调用工具,不需要关心这些细节。
工厂模式 让测试变得简单:写单元测试时可以注入 Mock model,不需要真实 API Key。
依赖方向 :
1 __init__.py → project.py → service.py → tools.py → repositories.py → models.py
所有箭头都指向"更稳定"的下层。models.py 不依赖任何其他文件,是最稳定的;__init__.py 依赖所有人,是最不稳定的。这符合稳定依赖原则 。
向后兼容 :
虽然内部拆成了 7 个文件,但外部导入完全不变:
1 2 from projects.p01_customer_service import query_order, CustomerServiceProject
这就是开闭原则 :对扩展开放,对修改关闭。
3.5 跑一跑:它真的行吗
3.5.1 工具单元测试(离线,不依赖模型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class TestQueryOrder : def test_order_found (self ): out = query_order.invoke({"order_id" : "10001" }) assert "无线耳机" in out and "已发货" in out and "299" in out def test_order_not_found (self ): out = query_order.invoke({"order_id" : "99999" }) assert "未找到" in out def test_order_db_error_graceful (self, monkeypatch ): """验证数据库异常时工具优雅降级而非崩溃。""" def _boom (*a, **kw ): raise sqlite3.Error("模拟数据库故障" ) monkeypatch.setattr (sqlite3, "connect" , _boom) out = query_order.invoke({"order_id" : "10001" }) assert "暂时不可用" in out
测试覆盖 :
正常查询(订单存在)
查询不存在的订单(返回"未找到")
数据库故障(优雅降级,不崩溃)——这是生产级代码的关键测试
3.5.2 集成测试(需要 API Key,调用真实模型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class TestAgentIntegration : def test_order_query_uses_tool (self ): project = CustomerServiceProject() reply = project.run("帮我查订单 10001 的状态" ) assert "10001" in reply def test_out_of_scope_escalates (self ): project = CustomerServiceProject() reply = project.run("我要投诉你们发货太慢" ) assert any (w in reply for w in ["转接" , "人工" , "客服" ]) def test_metrics_incremented (self ): project = CustomerServiceProject() before = project.metrics.total_runs project.run("查订单 10001" ) assert project.metrics.total_runs == before + 1
3.5.3 手工验收
在统一管理台中,选择「智能客服 Agent」,依次测试:
输入
预期行为
“帮我查订单 10001”
显示无线耳机、299 元、已发货
“那它的物流呢”
显示杭州转运中心
“能退货吗”
返回 7 天无理由退货政策
“我要投诉”
礼貌引导转人工
“你好”
友好问候,引导说明需求
3.6 送上线:让它上班
3.6.1 本地运行
1 2 3 4 cd backendcp .env.example .env uvicorn main:app --reload --port 8000
3.6.2 Docker 部署
1 2 export ANTHROPIC_API_KEY="sk-ant-..." docker compose up -d
3.6.3 生产部署 Checklist
[ ] 替换 SQLite 为 PostgreSQL(修改连接字符串)
[ ] 配置 ENVIRONMENT=production
[ ] 设置合理的流控和并发阈值
[ ] 接入日志聚合系统
[ ] 配置 Prometheus 抓取 /api/metrics
3.7 回头看:学到了什么
3.7.1 用到的能力回顾
能力
在本项目中的体现
Tool 工具调用
三个 @tool 装饰的函数,Agent 自动判断何时调用
Prompt 工程
精心设计的 System Prompt:角色、工具约束、语气、边界、错误传播
数据持久化
SQLite 三张表,可替换为 PostgreSQL
错误处理
工具级 try/except + 基类级重试 + 最终兜底,三层保护
流控
基类内置 per-thread 滑动窗口
指标
AgentMetrics 自动收集运行次数、错误率、耗时
3.7.2 常见坑
docstring 太笼统 ——Agent 不知道什么时候该用工具。docstring 要具体到操作对象和返回内容。
工具不返回"未找到" ——对不存在的订单返回空字符串,Agent 会困惑。工具要明确返回"未找到"。
System Prompt 缺少边界 ——不加"超出能力转人工",Agent 会试图处理它处理不了的事。
数据库连接不关闭 ——finally: conn.close() 是必须的,否则连接泄漏。
3.7.3 可扩展方向
接入真实数据库(PostgreSQL)
增加更多工具:create_return_order、query_coupon
多轮对话记忆:接入 LangGraph Checkpointer
RAG 升级:政策文本从硬编码改为向量检索
📌 项目一完成。 从需求分析到部署上线,我们完整走完了一个生产级 Agent 项目的开发全链路。这只是开始——下一个项目我们将深入 RAG ,用 LangGraph + LlamaIndex 构建一个能回答企业私有文档的「企业知识库问答 Agent」。
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !