1. 从零搭建智能客服系统为什么我选择 Agent 架构去年年底我接手了一个内部客服系统的重构项目需求很明确把原来基于规则匹配和简单意图分类的老系统换掉做一个能真正理解用户问题、能调用后端接口查数据、能多轮追问的智能客服。当时团队讨论了两条路线一条是继续走传统的 NLU 加对话管理那一套另一条是直接用 Agent 架构来做。我们最终选了后者做完之后踩了不少坑也积累了一些经验这里系统性地聊一聊。先说清楚这个系统是干什么的。它是一个面向内部业务线的客服助手用户主要是公司内部的运营人员和外部合作方的对接人。他们的问题集中在订单状态查询、账单明细核对、权限申请流程、接口报错排查这几类。以前的老系统只能处理固定话术用户问“我上周那笔订单为什么还没结算”系统就懵了因为它只认“查订单”这个意图不认“为什么还没结算”背后的因果追问。换成 Agent 架构之后核心变化在于系统不再只是做意图分类而是让模型自己决定下一步该干什么——是直接回答还是调用工具查数据还是追问澄清。Agent 这个词这两年热度很高但很多人对它的理解还停留在“套了个壳的聊天机器人”。我的理解是Agent 的核心在于自主决策加工具调用。普通对话系统是你问一句它答一句Agent 是你说一个目标它自己规划步骤、自己选工具、自己判断结果够不够、不够就继续查。这个差别在客服场景里特别明显。用户说“帮我看看昨天那批货到哪了”Agent 需要先确认是哪批货然后调物流接口拿到结果后判断是否需要补充说明最后组织语言回复。这一串动作里每一步的决策都是模型在做而不是代码里写死的 if-else。为什么不用传统方案我算过一笔账。老系统维护了大概两百多条规则每次业务调整都要改规则、重新测试、上线平均一个需求从提出到上线要三天。换成 Agent 之后大部分调整只需要改提示词和工具描述半天就能验证完。当然代价是推理成本上去了延迟也从原来的两百毫秒涨到了两三秒。但对于内部客服这种对实时性要求没那么极端的场景这个 trade-off 是划算的。适合谁来参考这些经验如果你正在做或者准备做 Agent 相关的开发尤其是客服、助手、自动化流程这类场景这些经验应该能帮你少走一些弯路。如果你只是对 Agent 感兴趣想了解它到底怎么落地也可以看看我会尽量把技术细节讲得通俗一些。2. 十条经验逐条拆解从提示词到工具设计2.1 提示词不是写作文是写接口文档刚开始做的时候我花了两天时间打磨系统提示词写得特别详细什么“你是一个专业的客服助手要热情、耐心、准确”之类的。结果上线后发现模型经常跑偏该调工具的时候不调不该回答的时候乱回答。后来我意识到一个问题提示词的本质不是描述角色而是定义行为边界。我重新组织了一版提示词结构变成了三块第一块是能力声明明确告诉模型它能做什么、不能做什么第二块是工具清单每个工具什么情况下用、参数怎么填第三块是输出格式规定它必须按什么结构返回。改完之后效果立竿见影。举个例子以前模型看到“帮我查一下订单”会直接编一个订单号回复现在它会先调query_order工具因为提示词里明确写了“任何涉及订单状态的回答必须先调用 query_order 获取真实数据禁止自行编造”。这里有个细节值得展开。工具描述不能只写“查询订单”要写清楚输入是什么、输出是什么、什么场景下用。我现在的写法是这样的{ name: query_order, description: 根据订单号或用户ID查询订单状态。当用户询问订单进度、结算状态、物流信息时使用此工具。如果用户没有提供订单号先调用 get_user_orders 获取用户最近订单列表。, parameters: { order_id: 订单号可选, user_id: 用户ID可选, status_filter: 状态筛选可选值pending, shipped, settled } }你看描述里把调用时机和前置依赖都写清楚了。模型看到这个描述就知道没订单号的时候该先干什么。这比在系统提示词里写一大段“如果用户没提供订单号你要先问”要有效得多因为工具描述是跟着工具走的模型在决策时直接能看到。提示工具描述里一定要写“什么时候用”而不是只写“这是什么”。前者影响模型决策后者只影响模型理解。2.2 工具设计要像给新人写操作手册工具设计这块我踩的坑最多。一开始我把后端接口直接包装成工具参数名跟接口文档一模一样什么biz_type、page_size、sort_field。结果模型经常填错参数要么把biz_type填成中文要么忘了传page_size导致返回巨量数据。后来我改了一个原则工具的参数设计要面向模型不是面向后端。模型不理解biz_type是什么但它理解“订单类型”。所以我把参数名全改成自然语言能理解的词并且在描述里给出可选值。比如{ name: search_orders, description: 搜索订单。用户说查订单看看订单订单列表时使用。, parameters: { order_type: { type: string, description: 订单类型可选值采购订单、销售订单、调拨订单, enum: [采购订单, 销售订单, 调拨订单] }, time_range: { type: string, description: 时间范围格式如最近7天本月上周 } } }还有一个关键点工具数量要克制。我一开始设计了二十多个工具结果模型选择困难经常调错。后来合并到八个核心工具每个工具的功能边界清晰准确率明显提升。经验值是单次对话中模型能稳定处理的工具数量在五到十个之间超过这个数就要考虑分组或者用路由层做预筛选。2.3 多轮对话的状态管理不能全靠模型Agent 做多轮对话有个天然优势就是它能记住上下文。但这里有个陷阱模型的记忆是不可靠的。我遇到过好几次用户在第一轮说了订单号第三轮模型就忘了开始重新问。原因是上下文窗口有限中间穿插了工具调用的返回结果把前面的信息挤掉了。我的解决方案是引入一个轻量的状态管理器。不是让模型自己记而是代码层面维护一个会话状态对象每次模型调用前把关键信息注入到提示词里。比如class SessionState: def __init__(self): self.order_id None self.user_id None self.last_tool_result None self.pending_question None def to_context(self): parts [] if self.order_id: parts.append(f当前订单号{self.order_id}) if self.user_id: parts.append(f当前用户ID{self.user_id}) if self.pending_question: parts.append(f待确认问题{self.pending_question}) return \n.join(parts)每次调用模型时把to_context()的结果拼到系统提示词后面。这样即使上下文被截断关键信息也不会丢。这个做法看起来笨但实测下来比让模型自己记靠谱得多。2.4 错误处理要区分“模型错”和“工具错”Agent 系统出错的时候排查起来比传统系统麻烦因为错误可能来自模型决策也可能来自工具执行。我一开始没区分所有错误都返回给模型让它重试结果模型有时候会陷入死循环反复调同一个失败的工具。后来我加了一层错误分类。工具执行失败分三种参数错误、业务错误、系统错误。参数错误返回给模型让它修正参数重试业务错误直接返回给用户比如“订单不存在”系统错误记录日志并返回兜底话术。模型决策错误则通过提示词约束来减少比如限制单次对话最多调用工具五次超过就强制返回。def handle_tool_error(error, tool_name, params): if isinstance(error, ParamError): return {status: retry, message: f参数错误{error.detail}请修正后重试} elif isinstance(error, BusinessError): return {status: final, message: error.user_message} else: logger.error(fTool {tool_name} failed: {error}) return {status: final, message: 系统繁忙请稍后再试}这个分类逻辑写起来简单但效果很好。模型拿到retry会重新组织参数拿到final就直接结束不会瞎折腾。2.5 评测集要覆盖“边界”而不是“典型”做 Agent 评测的时候我一开始准备了一百条典型问题比如“查订单”“查账单”测下来准确率百分之九十五感觉挺好。上线之后发现用户的问题根本不典型各种奇怪的问法都有。后来我重新设计评测集重点覆盖边界情况模糊指代、多意图混合、中途换话题、信息缺失、否定表达。举个例子“那个东西还没到吗”这种问题没有主语需要模型结合上下文推断“那个东西”是什么。还有“我不要查订单我要查账单”否定表达容易让模型误判。这些边界 case 才是真正考验 Agent 能力的地方。我现在评测集里典型问题只占三成七成都是边界和异常情况。提示评测集要定期更新把线上真实出现的 bad case 加进去。我每周会抽一批线上对话人工标注补充到评测集里。2.6 提示词版本管理比代码版本管理还重要这个经验是用血换来的。有一次我改了一版提示词测试环境跑得好好的上线后客服质量突然下降。排查了半天才发现是提示词改动影响了工具调用的触发条件。但因为没有版本记录我根本不知道改之前是什么样。现在我所有提示词都走 Git 管理每次改动必须写 commit message说明改了什么、为什么改、预期效果是什么。而且提示词文件和代码分开仓库因为提示词的迭代频率比代码高得多混在一起容易乱。# 提示词仓库结构 prompts/ system/ v1.0.0.md v1.1.0.md current - v1.1.0.md tools/ query_order.md search_orders.md eval/ test_cases.jsonl results/每次上线前跑一遍评测集对比新旧版本的准确率、工具调用次数、平均轮次。如果新版本在某个指标上下降超过百分之五就不允许上线。2.7 延迟优化要从“并行”和“缓存”下手Agent 系统的延迟主要来自模型推理和工具调用。模型推理这块能优化的空间不大除非换更小的模型或者做量化。但工具调用这块有很多可以做的。第一个是并行调用。有些场景下模型需要同时查多个数据源比如查订单的同时查物流。如果串行调用延迟就是两次之和并行调用就是取最大值。我在工具层加了一个parallel标记模型可以一次性发起多个工具调用请求代码层并行执行后合并结果返回。第二个是缓存。客服场景里有很多重复查询比如同一个订单号被不同用户查、同一个用户反复查同一个订单。我在工具层加了一层 Redis 缓存对于查询类工具结果缓存五分钟。命中率大概在百分之三十左右对降低延迟有帮助。cache(ttl300) def query_order(order_id): return backend_api.get_order(order_id)第三个是流式输出。模型生成回复的时候用流式返回用户能看到字一个个蹦出来感知延迟会低很多。虽然总时间没变但体验好很多。2.8 安全边界要靠“白名单”而不是“黑名单”Agent 能调工具就意味着它能对后端系统产生实际影响。如果工具里有写操作比如修改订单状态、发送通知那安全边界就特别重要。我一开始用黑名单列出禁止模型做的事情比如“不能删除订单”“不能修改金额”。但黑名单的问题是永远列不全模型总能找到你没想到的路径。后来改成白名单每个工具明确声明它允许被哪些角色调用、允许在什么条件下调用。模型在调用前代码层先做一次权限校验不通过就直接拒绝不把请求发给工具。TOOL_PERMISSIONS { query_order: {roles: [agent, admin], conditions: []}, update_order_status: { roles: [admin], conditions: [order.status ! settled] } }这样即使模型被诱导去调不该调的工具代码层也会拦住。安全这件事不能指望模型自觉。2.9 日志要记录“决策过程”而不只是“结果”Agent 的日志如果只记输入输出排查问题的时候会很痛苦。因为你不知道模型为什么选了那个工具、为什么没选另一个。我现在日志里会记录完整的决策链路模型收到的完整提示词、模型的原始输出、解析后的工具调用请求、工具返回结果、最终回复。{ session_id: abc123, turn: 3, prompt_tokens: 1520, model_output: { thought: 用户询问订单状态需要先获取订单号, action: get_user_orders, params: {user_id: U001} }, tool_result: {orders: [{id: O123, status: shipped}]}, final_response: 您最近的订单 O123 已发货... }这个日志量很大但排查问题的时候真的救命。我建议至少保留最近七天的完整日志冷存储保留三十天。2.10 上线只是开始持续迭代才是常态Agent 系统跟传统系统最大的区别是它没有“完成”这个状态。传统系统上线后只要不出 bug 就不用管Agent 系统上线后你会发现模型每天都在给你“惊喜”——有些是好的比如它自己学会了新的问法有些是坏的比如它开始编造数据。我现在保持每周一次迭代的节奏周一收集线上 bad case周二到周三分析原因、调整提示词或工具周四跑评测集周五灰度上线。这个节奏听起来累但比攒一个月再大改要轻松得多因为每次改动小出问题也容易回滚。3. 实操过程从零到一搭建一个最小可用 Agent3.1 环境准备与技术选型先说技术栈。模型这块我用的是支持 function calling 的大模型 API具体哪家就不说了选型标准是三点支持工具调用、上下文窗口够大、价格能接受。框架这块我试过几个流行的 Agent 框架最后决定不用框架自己写调度逻辑。原因很简单框架抽象层太厚出问题不好排查而且客服场景的调度逻辑并不复杂自己写反而更可控。核心依赖就几个pip install openai # 模型调用 pip install fastapi # API 服务 pip install redis # 缓存和会话状态 pip install pydantic # 参数校验项目结构大概是这样agent_service/ main.py # FastAPI 入口 agent/ core.py # Agent 调度核心 prompts.py # 提示词管理 tools.py # 工具定义与注册 state.py # 会话状态管理 eval/ run_eval.py # 评测脚本 cases.jsonl # 评测用例3.2 核心调度循环的实现Agent 的核心就是一个循环模型输出 - 解析 - 执行工具 - 结果回填 - 再输出。我把它写成了一个run_agent函数def run_agent(session_id, user_input, max_turns5): state load_state(session_id) messages build_messages(state, user_input) for turn in range(max_turns): response call_model(messages, toolsTOOL_SCHEMAS) action parse_action(response) if action.type final: save_state(session_id, state) return action.content if action.type tool_call: result execute_tool(action.name, action.params, state) messages.append({role: tool, content: result}) continue # 异常情况兜底 return 抱歉我暂时无法处理这个问题请换个方式描述。 return 这个问题比较复杂建议您联系人工客服。这个循环里几个关键点max_turns限制防止死循环parse_action要能处理模型输出的各种格式偏差execute_tool里做权限校验和错误分类。3.3 提示词模板的组装逻辑提示词不是写死的字符串而是动态组装的。我把它拆成四层def build_system_prompt(state): parts [ BASE_PROMPT, # 基础能力声明 TOOL_DESCRIPTIONS, # 工具清单 state.to_context(), # 会话状态 OUTPUT_FORMAT # 输出格式要求 ] return \n\n.join(parts)BASE_PROMPT大概长这样你是一个客服助手负责帮助用户查询订单、账单、权限等信息。 规则 1. 任何涉及具体数据的回答必须先调用工具获取真实数据禁止编造。 2. 如果用户问题缺少必要信息先追问澄清不要猜测。 3. 单次回复不超过三句话保持简洁。 4. 如果工具返回错误根据错误类型决定是重试还是告知用户。OUTPUT_FORMAT规定模型必须返回 JSON{ thought: 你的思考过程, action: final 或 tool_call, tool_name: 工具名action 为 tool_call 时必填, tool_params: 工具参数action 为 tool_call 时必填, content: 最终回复action 为 final 时必填 }这个格式约束很重要它让代码层能稳定解析模型输出不用做复杂的文本匹配。3.4 工具注册与执行的完整流程工具注册我用装饰器模式定义的时候顺便把 schema 生成了TOOL_REGISTRY {} def tool(name, description, params_schema): def decorator(func): TOOL_REGISTRY[name] { func: func, schema: { name: name, description: description, parameters: params_schema } } return func return decorator tool( namequery_order, description根据订单号查询订单状态。用户询问订单进度时使用。, params_schema{ order_id: {type: string, description: 订单号} } ) def query_order(order_id): return backend.get_order(order_id)执行的时候先查注册表再做权限校验最后调函数def execute_tool(name, params, state): if name not in TOOL_REGISTRY: return {error: 未知工具} if not check_permission(name, state): return {error: 无权限调用此工具} try: result TOOL_REGISTRY[name][func](**params) return {result: result} except ParamError as e: return {error: f参数错误{e}, retry: True} except BusinessError as e: return {error: e.user_message, retry: False}3.5 会话状态的持久化方案会话状态我用 Redis 存key 是session:{session_id}value 是 JSON 序列化的状态对象。过期时间设三十分钟因为客服对话一般不会超过这个时长。def save_state(session_id, state): redis.setex( fsession:{session_id}, 1800, json.dumps(state.to_dict()) ) def load_state(session_id): data redis.get(fsession:{session_id}) if data: return SessionState.from_dict(json.loads(data)) return SessionState()这里有个细节状态更新要即时写回。我一开始是对话结束后统一写结果中途服务重启状态就丢了。改成每次工具调用后都写一次虽然 Redis 压力大一点但可靠性高很多。3.6 评测脚本的编写与运行评测脚本的核心是批量跑用例、对比预期和实际、输出报告def run_eval(cases_file, agent_version): cases load_cases(cases_file) results [] for case in cases: session_id feval_{case[id]} actual run_agent(session_id, case[input]) results.append({ id: case[id], input: case[input], expected: case[expected], actual: actual, pass: judge(actual, case[expected]) }) report generate_report(results) save_report(report, agent_version) return reportjudge函数我用了两种方式精确匹配用于简单 case模型评判用于复杂 case。模型评判就是让另一个模型判断实际回复是否满足预期虽然有点绕但对于开放式问题比字符串匹配靠谱。4. 常见问题与排查技巧实录4.1 模型不调工具直接编答案怎么办这是最常见的问题。模型倾向于直接回答而不是调工具因为调工具要生成更多 token而且它“觉得”自己知道答案。解决办法有三个层次第一层是提示词约束在系统提示词里明确写“禁止编造数据必须调工具”。第二层是工具描述优化把调用时机写清楚。第三层是代码层强制如果模型返回了 final 但问题涉及数据查询代码层拦截并强制重新调用。我现在的做法是第三层兜底维护一个关键词列表如果用户输入包含“查”“看看”“多少”“状态”等词但模型没有调工具就返回了 final代码层直接拒绝这个回复重新提示模型调工具。4.2 工具调用参数填错怎么排查参数填错通常有三种原因参数名不直观、可选值没给全、模型理解偏差。排查的时候先看日志里模型输出的原始参数对比工具 schema看是哪个环节出的问题。如果是参数名问题改成自然语言友好的名字。如果是可选值问题在 schema 里加enum约束。如果是模型理解偏差在工具描述里加例子。我现在的工具描述里都会带一两个调用示例效果很好。4.3 多轮对话中信息丢失怎么解决前面提过用状态管理器但还有一种情况是模型主动“忘记”。比如用户说“就那个订单”模型不知道“那个”指什么。这时候需要在状态里记录最近提到的实体并在提示词里显式提醒。我的做法是在状态里维护一个recent_entities列表记录最近三轮提到的订单号、用户ID等。每次组装提示词时如果用户输入包含指代词就把recent_entities注入进去。4.4 模型陷入死循环反复调同一个工具这个问题的根源通常是工具返回的错误信息不够明确模型不知道该怎么修正。解决办法是在错误信息里给出明确的修正方向。比如不要返回“参数错误”而是返回“order_id 格式不正确应该是纯数字请重新提取”。另外加一个调用计数器同一个工具连续调用超过三次就强制中断返回兜底话术。4.5 线上延迟突然升高怎么定位延迟升高一般来自三个地方模型推理变慢、工具调用变慢、并发量上来。排查顺序是先看监控面板区分是模型侧还是工具侧。模型侧看 token 数和响应时间工具侧看每个工具的平均耗时。我遇到过几次工具侧延迟升高最后发现是某个后端接口变慢了。解决办法是给每个工具加超时超过两秒直接返回超时错误不让它拖累整个对话。问题现象可能原因排查方法解决方案模型不调工具提示词约束不够看日志中模型输出加关键词拦截兜底参数填错参数名不直观对比 schema 和实际参数改参数名、加 enum信息丢失上下文被截断看提示词 token 数状态管理器注入死循环错误信息不明确看工具返回内容明确修正方向、加计数器延迟升高工具或模型变慢看分项耗时监控加超时、加缓存4.6 几个我踩过的坑和对应的技巧第一个坑是提示词里用了太多“不要”“禁止”这类否定词。模型对否定词的处理能力比想象中弱你说“不要编造”它可能反而更容易编造。后来我改成正面表述“所有数据必须来自工具返回结果”。第二个坑是工具返回的数据结构太复杂。我一开始把后端接口的完整 JSON 返回给模型结果模型被一堆无关字段干扰。后来改成只返回关键字段并且用自然语言组织一下再给模型。第三个坑是评测集和线上分布不一致。评测集里都是标准问法线上全是口语化表达。解决办法是定期从线上采样人工标注后补充到评测集。第四个坑是忽略了模型的“创造力”。有一次模型在回复里加了一句“祝您生活愉快”虽然没错但显得很假。后来在提示词里明确规定了语气和结尾方式不允许自由发挥。提示Agent 开发里模型的能力是上限提示词和工具设计是下限。下限做不好上限再高也没用。5. 关于 Agent 开发的一点个人体会做完这个项目之后我最大的感受是Agent 开发跟传统软件开发是两种思维。传统开发是确定性的你写什么代码就执行什么逻辑Agent 开发是概率性的你定义的是边界和约束具体怎么走是模型决定的。这意味着你不能用传统测试的思路来验证 Agent也不能用传统运维的思路来维护 Agent。我现在更愿意把 Agent 当成一个“新人”来带。你给它写提示词就像给新人写操作手册你设计工具就像给新人配权限你做评测就像给新人做考核。它会有理解偏差会有自作主张的时候也会有你意想不到的发挥。你要做的是把边界划清楚把反馈给及时然后接受它偶尔的不完美。还有一个体会是Agent 开发里最值钱的不是模型本身而是你对业务的理解。模型是通用的但工具是业务的提示词是业务的评测集也是业务的。谁对业务理解深谁就能做出好用的 Agent。技术只是手段业务才是核心。最后分享一个我最近在试的方向用 Agent 来做 Agent 的评测。让一个模型扮演用户去跟 Agent 对话另一个模型做裁判自动生成评测报告。这样能大幅降低人工评测的成本而且能覆盖更多边界情况。目前还在实验阶段效果好的话再单独写一篇。