1. 从 Demo 到生产:Agent 落地为什么总在同一个地方翻车
做过 Agent 项目的人大概都有过这种体验:本地跑 Demo 的时候,工具调用丝滑、多轮对话连贯、任务完成率高得让人兴奋,恨不得当天就写周报汇报。结果一上生产环境,用户量稍微起来一点,各种问题就像约好了一样集中爆发——工具调用超时、上下文爆炸、权限越界、评测指标全线飘红。这不是个例,而是当前 Agent 从原型到生产落地过程中最普遍的困境。
我自己在过去一年多的时间里,先后参与过客服工单自动处理、内部知识库问答、数据分析助手等几个 Agent 项目的落地,踩过的坑可以说覆盖了从工具调用到权限安全、从上下文管理到评测可观测的完整链路。这篇文章不打算讲什么“Agent 未来已来”的宏大叙事,而是想把 Demo 惊艳、上线拉胯这件事拆开揉碎,把根因讲清楚,再把工程解法一条条摆出来。
如果你正在做 Agent 开发,或者团队正准备把 Agent 项目推向生产环境,这篇文章里提到的四道坎——工具调用、权限与安全、上下文与成本、评测与可观测——大概率你都会遇到。我会尽量用从业者之间交流的方式,把每个问题的表现、根因和工程解法讲透,附带可以直接参考的配置和参数。
2. 第一道坎:工具调用的稳定性与编排逻辑
2.1 Demo 里工具调用为什么看起来很美好
在 Demo 阶段,工具调用通常只有两三个,参数简单,调用链路短,而且测试用例是精心挑选的。比如一个天气查询 Agent,工具就是get_weather(city),输入输出都很确定,模型只要正确识别城市名就能调通。这种场景下,工具调用的成功率轻松跑到 95% 以上,给人造成一种“Agent 已经能用了”的错觉。
但生产环境的工具调用完全是另一回事。我经手的一个客服 Agent 项目,光是工具就有十几个:查订单、查物流、改地址、发起退款、查优惠券、转人工等等。每个工具的参数结构不同,有些还需要多步依赖——比如改地址之前必须先查订单确认状态,发起退款之前要校验订单是否在退款期内。这时候工具调用的失败率会从 Demo 的 5% 飙升到 30% 甚至更高。
2.2 工具调用翻车的三个典型根因
第一个根因是工具描述与模型理解之间的鸿沟。很多团队写工具描述的时候,习惯用工程师视角的简洁表达,比如query_order(order_id: string),但模型并不知道order_id的格式是什么、什么情况下该调用这个工具、调用失败后该怎么处理。我见过最离谱的一个案例,工具描述只写了一句话“查询订单信息”,结果模型在用户问“我的快递到哪了”的时候也去调这个工具,因为描述里没有明确区分订单查询和物流查询的边界。
第二个根因是多工具编排的依赖关系没有被显式管理。Demo 阶段通常是单工具调用,模型一次只调一个工具,拿到结果就回复。但生产环境经常需要链式调用:先查订单,再根据订单状态决定是否查物流,最后根据物流信息决定是否发起补偿。这种依赖关系如果完全交给模型去“自由发挥”,稳定性会非常差。我实测下来,纯靠模型自主编排的多步任务,端到端成功率通常只有 60% 到 70%。
第三个根因是工具调用的超时与重试策略缺失。Demo 阶段工具都是本地 mock,响应时间可以忽略不计。但生产环境的工具背后可能是第三方 API、数据库查询或者内部微服务,响应时间从几百毫秒到几秒不等。如果没有合理的超时设置和重试策略,一个慢查询就能把整个 Agent 的响应时间拖垮,用户体验直线下降。
2.3 工程解法:用状态机约束编排,用结构化描述提升调用准确率
针对工具描述的问题,我的经验是把工具描述当成 Prompt 来写,而不是当成 API 文档来写。具体来说,每个工具的描述需要包含四个要素:这个工具做什么、什么情况下应该调用、参数的具体格式和约束、调用失败后的建议处理方式。比如query_order的描述可以写成:“根据订单号查询订单的详细状态,包括支付状态、发货状态和退款状态。当用户询问订单相关问题且提供了订单号时调用。订单号格式为 18 位数字,如果用户没有提供订单号,先调用get_user_orders获取用户最近订单列表。”
针对多工具编排的问题,LangGraph 这类基于状态机的编排框架是目前比较靠谱的选择。它的核心思路是把 Agent 的执行流程显式定义成一张图,每个节点是一个工具调用或一个决策点,边表示状态转移条件。这样做的好处是,编排逻辑不再依赖模型的“自由发挥”,而是由代码来保证。模型只负责在特定节点做决策,比如“订单状态是已发货,下一步应该查物流还是直接回复”,而不是让它自己决定整个调用链路。
下面是一个简化的 LangGraph 编排示例,展示如何把订单查询和物流查询串起来:
from langgraph.graph import StateGraph, END from typing import TypedDict, Literal class AgentState(TypedDict): user_input: str order_id: str order_status: str logistics_info: str final_response: str def extract_order_id(state: AgentState): # 从用户输入中提取订单号 order_id = llm_extract(state["user_input"]) return {"order_id": order_id} def query_order(state: AgentState): result = order_api.query(state["order_id"]) return {"order_status": result["status"]} def should_query_logistics(state: AgentState) -> Literal["query_logistics", "generate_response"]: if state["order_status"] == "shipped": return "query_logistics" return "generate_response" def query_logistics(state: AgentState): result = logistics_api.query(state["order_id"]) return {"logistics_info": result["info"]} def generate_response(state: AgentState): response = llm_generate(state) return {"final_response": response} graph = StateGraph(AgentState) graph.add_node("extract_order_id", extract_order_id) graph.add_node("query_order", query_order) graph.add_node("query_logistics", query_logistics) graph.add_node("generate_response", generate_response) graph.set_entry_point("extract_order_id") graph.add_edge("extract_order_id", "query_order") graph.add_conditional_edges("query_order", should_query_logistics) graph.add_edge("query_logistics", "generate_response") graph.add_edge("generate_response", END) app = graph.compile()这个例子里,should_query_logistics这个条件边就是编排逻辑的核心。它用代码保证了“只有订单状态是已发货才查物流”,而不是让模型去猜。实测下来,这种显式编排能把多步任务的端到端成功率从 65% 左右提升到 90% 以上。
注意:状态机编排会增加代码复杂度,不是所有场景都值得上。如果工具数量少于 5 个、调用链路基本是单步的,直接用模型自主调用反而更简单。我的经验判断标准是:当工具之间存在依赖关系、或者需要根据中间结果做分支决策时,才引入状态机编排。
2.4 超时与重试的实操参数
工具调用的超时和重试策略,我一般按这个原则来配:读操作超时 3 秒、重试 2 次;写操作超时 5 秒、重试 1 次。读操作比如查询类工具,失败重试的成本低,可以多试几次;写操作比如退款、改地址,重试可能造成重复操作,所以要谨慎。
重试的间隔建议用指数退避,第一次重试等 500 毫秒,第二次等 1.5 秒,避免瞬间打爆下游服务。另外,重试之前一定要判断错误类型——网络超时可以重试,参数错误重试多少次都没用,直接返回错误让模型处理更合理。
3. 第二道坎:权限与安全,Agent 落地的隐形红线
3.1 Agent 权限问题的特殊性
传统应用的权限模型是“用户有什么权限,应用就有什么权限”,边界很清晰。但 Agent 的权限问题要复杂得多,因为 Agent 会自主调用工具,而工具背后可能是敏感操作。一个客服 Agent 如果被赋予了退款权限,理论上它可以对任何订单发起退款,这显然是不可接受的。
我见过一个真实的案例:某团队的内部数据分析 Agent,为了方便,直接用了管理员的数据库账号。结果模型在一次对话中误判了用户意图,执行了一条删除操作,虽然最后通过备份恢复了,但这件事让整个团队对 Agent 的权限管理重视了起来。
3.2 权限控制的三层防线
我的经验是,Agent 的权限控制需要三层防线,缺一不可。
第一层是工具级别的权限声明。每个工具在注册的时候,就要明确标注它的风险等级和所需权限。比如查询类工具标记为read,修改类工具标记为write,删除类工具标记为dangerous。Agent 在执行工具之前,先检查当前会话是否具备对应权限。
第二层是参数级别的校验。即使有权限调用某个工具,参数也要做校验。比如退款工具,要校验退款金额是否超过订单金额、退款次数是否超过限制、订单是否在退款期内。这些校验不能只靠模型判断,必须在工具实现层面硬编码。
第三层是操作级别的审计与确认。对于高风险操作,比如删除数据、大额退款、修改关键配置,建议引入人工确认环节。Agent 可以生成操作建议,但最终执行需要用户明确确认。这个确认环节在 Demo 阶段经常被省略,但生产环境绝对不能省。
3.3 一个可参考的权限配置方案
下面是一个基于角色的 Agent 权限配置示例,用 YAML 格式定义:
roles: customer_service: allowed_tools: - query_order - query_logistics - query_coupon restricted_tools: - name: modify_address conditions: - order_status in ["pending", "paid"] - max_modifications: 1 - name: initiate_refund conditions: - order_status in ["paid", "shipped"] - max_refund_amount: 500 - require_confirmation: true denied_tools: - delete_order - modify_price data_analyst: allowed_tools: - query_database - generate_chart restricted_tools: - name: export_data conditions: - max_rows: 10000 - require_approval: true denied_tools: - drop_table - update_record这个配置的核心思路是:默认拒绝,显式允许。不在allowed_tools里的工具,Agent 一律不能调用。restricted_tools里的工具需要满足特定条件才能调用,denied_tools则是明确禁止的。
提示:权限配置一定要和业务方一起评审,不能由开发团队单方面决定。我踩过的坑是,开发觉得某个工具风险不高就放开了,结果业务方看到后惊出一身冷汗。权限边界是业务问题,不是技术问题。
3.4 安全审计日志的设计要点
Agent 的审计日志和传统应用不一样,需要额外记录几个关键信息:模型的决策理由、工具调用的完整参数、调用前后的状态变化、以及最终的用户反馈。这些信息在排查问题时非常关键。
我一般会把审计日志设计成结构化的 JSON,每条记录包含session_id、timestamp、tool_name、parameters、model_reasoning、result_status、user_feedback这几个字段。其中model_reasoning是模型决定调用这个工具时的思考过程,虽然模型的自述不一定完全准确,但在复盘时能提供很多线索。
日志的存储建议用支持全文检索的方案,比如 Elasticsearch 或者带全文索引的关系型数据库。因为排查问题时经常需要按关键词搜索,比如“所有涉及退款的调用”、“所有参数包含特定订单号的调用”,没有全文检索会非常痛苦。
4. 第三道坎:上下文管理与成本控制的平衡术
4.1 上下文爆炸是怎么发生的
Agent 的上下文消耗比普通对话应用快得多,原因有三个:工具调用的输入输出会占用大量 token、多轮对话的历史会不断累积、以及为了保持任务连贯性需要携带的状态信息。我实测过一个客服 Agent,平均每轮对话消耗的 token 是普通问答的 5 到 8 倍,因为每轮都可能触发工具调用,而工具返回的结果往往很长。
上下文爆炸的直接后果是成本飙升和响应变慢。更隐蔽的后果是,当上下文接近模型窗口上限时,模型的表现会明显下降,出现“忘记前面说过什么”、“重复调用同一个工具”等问题。这个现象在 Demo 阶段很难发现,因为 Demo 的对话轮次通常很少。
4.2 上下文管理的四种策略
第一种是滑动窗口加摘要。保留最近 N 轮完整对话,更早的对话用模型生成摘要。N 的取值建议根据任务复杂度来定,简单问答 5 到 8 轮,复杂任务 10 到 15 轮。摘要的 prompt 要明确要求保留关键信息,比如订单号、用户诉求、已执行的操作等。
第二种是工具结果的压缩。工具返回的原始结果往往包含大量冗余信息,比如查询订单返回的 JSON 可能有几十个字段,但 Agent 真正需要的只有订单状态、金额、时间这几个。可以在工具层面做一层过滤,只返回必要字段,或者用模型对工具结果做一次摘要。
第三种是状态外置。把任务相关的状态信息存到外部存储,比如 Redis 或者数据库,上下文里只保留状态的引用。这样即使对话很长,上下文也不会无限膨胀。LangGraph 的 checkpointer 机制就是这种思路,它把每一步的状态持久化,需要的时候再加载。
第四种是分层上下文。把上下文分成系统层、任务层、对话层三层。系统层是固定的 prompt 和工具描述,任务层是当前任务的目标和约束,对话层是最近几轮交互。不同层级的更新频率和保留策略不同,系统层基本不变,任务层在任务切换时更新,对话层滚动更新。
4.3 成本控制的实操参数
成本控制的核心是在效果和成本之间找平衡点。我的经验是,先保证效果,再优化成本,不要一上来就为了省钱牺牲效果。
具体参数上,我一般这样配:系统 prompt 控制在 500 token 以内,工具描述每个控制在 100 token 以内,工具结果压缩到 200 token 以内,对话历史保留最近 10 轮,更早的用 200 token 以内的摘要替代。这样下来,单轮对话的上下文消耗大概在 2000 到 3000 token,比不优化的情况能省 60% 到 70%。
另外,模型分级使用也是重要的成本控制手段。不是所有环节都需要用最强的模型,比如意图识别、参数提取这些相对简单的任务,可以用小模型;只有复杂的推理和决策环节才用大模型。我实测下来,这种分级策略能在效果基本不变的情况下,把成本降低 40% 左右。
注意:上下文压缩和摘要会引入信息损失,一定要在评测环节验证压缩后的效果。我踩过的坑是,为了省成本把工具结果压缩得太狠,结果模型因为缺少关键信息而做出错误决策,反而造成了更大的损失。
5. 第四道坎:评测与可观测,Agent 上线的最后一道保险
5.1 为什么传统评测方法对 Agent 不够用
传统应用的评测是确定性的:输入 A 必然得到输出 B,测试用例通过就是通过。但 Agent 的输出是不确定的,同一个输入可能因为模型采样的随机性、工具返回的差异、上下文的变化而产生不同的结果。用传统的断言式测试来评测 Agent,要么过于宽松(只检查关键词),要么过于严格(要求完全匹配),都不太实用。
Agent 的评测需要一套新的方法论,核心是从结果评测转向过程评测。不仅要看最终回复对不对,还要看中间的工具调用是否合理、参数是否正确、编排逻辑是否符合预期。
5.2 评测体系的三个层次
第一层是单元评测,针对单个工具调用。构造一批测试用例,每个用例包含用户输入、期望调用的工具、期望的参数。这一层主要验证模型的工具选择能力和参数提取能力。我一般会准备 100 到 200 个用例,覆盖正常场景和边界场景。
第二层是链路评测,针对多步任务。构造一批端到端的任务,每个任务包含初始输入、期望的最终结果、以及关键的中间步骤。这一层验证的是编排逻辑和任务完成能力。用例数量可以少一些,50 到 100 个,但每个用例要足够复杂。
第三层是线上评测,针对真实流量。通过 A/B 测试、用户反馈、人工抽检等方式,持续监控 Agent 在真实场景下的表现。这一层最重要,也最容易被忽略。我见过不少团队,离线评测做得很好,但上线后没有持续监控,出了问题很久才发现。
5.3 可观测性的关键指标
Agent 的可观测性需要监控的指标比传统应用多得多,我一般会关注这几类:
| 指标类别 | 具体指标 | 告警阈值建议 |
|---|---|---|
| 调用成功率 | 工具调用成功率 | 低于 95% 告警 |
| 响应时间 | P50/P95/P99 响应时间 | P95 超过 5 秒告警 |
| 上下文消耗 | 单轮平均 token 数 | 超过预算 80% 告警 |
| 成本 | 单次对话平均成本 | 超过预算 80% 告警 |
| 任务完成率 | 端到端任务成功率 | 低于 85% 告警 |
| 异常率 | 模型输出解析失败率 | 超过 2% 告警 |
| 用户反馈 | 负面反馈率 | 超过 10% 告警 |
这些指标需要做成实时看板,并且配置告警。我特别想强调的是模型输出解析失败率这个指标,它经常被忽略,但往往是问题的早期信号。当模型开始输出不符合预期格式的内容时,通常意味着上下文出了问题,或者模型对当前任务的理解出现了偏差。
5.4 排查问题的实操思路
Agent 出问题的时候,排查思路和传统应用很不一样。我的经验是从后往前查:先看最终输出是什么,再看最后一步工具调用的结果,然后看模型的决策理由,最后看上下文里有什么信息。
具体来说,遇到“Agent 回复不对”的问题,我会按这个顺序排查:
- 检查最终输出,确认问题现象
- 查看最后一步工具调用的参数和结果,确认工具是否正常
- 查看模型在调用工具前的 reasoning,确认决策逻辑是否合理
- 检查上下文,确认模型是否获得了足够的信息
- 检查系统 prompt 和工具描述,确认是否有歧义
这个顺序能覆盖 80% 以上的问题。剩下的 20% 可能是模型本身的随机性问题,需要通过调整 temperature 参数或者增加 few-shot 示例来解决。
提示:排查 Agent 问题时,一定要保留完整的调用链路日志,包括模型的输入输出、工具的输入输出、以及中间的状态变化。没有这些日志,排查基本靠猜。我建议在开发阶段就把日志打全,不要等到出问题才补。
6. 从四道坎到落地:一些实战体会
6.1 落地节奏比技术选型更重要
我见过不少团队在技术选型上纠结很久,LangGraph 还是 AutoGen、自研还是用框架,但真正影响落地成败的往往是节奏。我的建议是先跑通最小闭环,再逐步加固。不要一上来就追求完美的权限体系、完整的评测框架,先把核心流程跑通,让业务方看到价值,然后再一步步补上工程化的部分。
具体节奏上,我一般分三个阶段:第一阶段用最简单的方案跑通 Demo,验证业务价值;第二阶段加上权限、超时、重试这些基础工程能力,小范围试点;第三阶段完善评测、可观测、成本控制,正式上线。每个阶段大概两到四周,不要跳步。
6.2 业务方的参与度决定落地深度
Agent 项目和其他技术项目最大的区别是,它和业务的耦合度非常高。工具怎么设计、权限怎么划分、评测标准怎么定,这些都需要业务方深度参与。我经历过最顺利的项目,是业务方派了一个产品经理全程跟进,每个工具的语义、每个权限的边界都一起评审。最不顺利的项目,是业务方只在需求阶段出现,后面全是开发自己拍脑袋,结果上线后业务方各种不满意。
6.3 保持对模型能力的合理预期
最后想说的一点是,Agent 不是万能的,模型的能力边界是客观存在的。有些任务模型就是做不好,比如需要精确计算、需要长期记忆、需要复杂推理的场景。遇到这种情况,与其硬用 Agent,不如退回到传统方案,或者用 Agent 加人工兜底的方式。我见过一些团队,明明一个规则引擎就能解决的问题,非要用 Agent,结果又贵又不稳定。
Agent 真正擅长的,是那些需要理解自然语言、需要灵活处理、但容错率相对较高的场景。比如客服问答、信息检索、辅助决策。在这些场景里,Agent 能发挥出传统方案无法比拟的优势。找准场景,比堆技术更重要。
6.4 一个容易被忽略的细节:工具返回值的格式
这个细节很小,但影响很大。工具返回值的格式如果不统一,模型处理起来会很吃力。我建议所有工具都返回结构化的 JSON,并且保持字段命名的一致性。比如查询类工具统一返回{"status": "success", "data": {...}, "message": ""},错误统一返回{"status": "error", "data": null, "message": "具体错误信息"}。这样模型在处理不同工具的结果时,不需要额外学习每种工具的返回格式,能显著提升稳定性。
我在一个项目里做过对比测试,统一返回值格式后,模型对工具结果的理解准确率从 82% 提升到了 94%。这个改动成本很低,但收益很明显,建议在项目初期就定好规范。
6.5 关于 Agent 记忆的一些实践
Agent 记忆是最近讨论很多的话题,我的实践体会是:短期记忆靠上下文,长期记忆靠外部存储,但长期记忆的写入和读取都需要谨慎设计。不是所有信息都值得记住,也不是所有记住的信息都值得在每次对话中读取。
我一般会把长期记忆分成两类:用户偏好类(比如用户习惯用的语言、偏好的回复风格)和事实类(比如用户的历史订单、之前咨询过的问题)。用户偏好类可以在每次对话开始时注入上下文,事实类则按需检索。检索的触发条件要设计得保守一些,宁可少检索,也不要检索一堆无关信息干扰模型。
记忆的更新策略也很关键。我建议采用追加为主、合并为辅的策略,新信息先追加,定期做一次合并和清理。不要每次对话都去更新记忆,那样容易造成记忆混乱。
6.6 最后分享一个排查工具调用问题的技巧
当 Agent 的工具调用出现问题时,我常用的一个技巧是把模型的 reasoning 单独拿出来看。很多模型在调用工具之前会输出一段思考过程,这段内容往往能直接暴露问题。比如模型说“用户想查订单,我应该调用 query_logistics”,这就说明工具描述有歧义,模型分不清订单查询和物流查询。
如果模型没有输出 reasoning,可以在 prompt 里显式要求它在调用工具前先说明理由。这个改动会增加一些 token 消耗,但排查问题时非常值得。等系统稳定后,可以再把 reasoning 关掉来省成本。
工具调用的问题,十有八九能在 reasoning 里找到线索。这个技巧帮我省了很多排查时间,推荐你也试试。