Agent工程化四板斧:从Demo到生产的五大鸿沟与解法
1. 约四成失败率背后,Agent 项目到底难在哪
这两年做 Agent 的团队越来越多了,但一个现实问题也摆到了台面上:很多 Agent 项目在 Demo 阶段跑得很好,一旦进入真实业务,就推进不下去。
技术社区和不少团队的复盘里,Agent 项目失败率高是一个高频话题,行业里大致流传着一个判断:约四成 Agent 项目最终没能落地。这个数字未必精确,但它确实反映了一个普遍感受——Agent 项目“看起来容易,做起来难”。
难在哪?模型能力不够?模型迭代其实已经很快了。Context 不够长?现在各家模型都在卷上下文窗口。真正的问题往往出现在工程侧:Agent 系统无法稳定地完成用户期望的任务,无法被测试、被观测、被约束、被回滚。
先理清一个概念。这里说的 Agent,指的是以大语言模型为决策核心,能够自主规划、调用工具、执行动作并基于反馈继续迭代的智能体系统。它和传统软件的关键区别在于:传统逻辑是确定的,if-else 写死之后,输入相同输出就相同;而 Agent 的核心循环(Agent loop)——感知、推理、行动、观察、再推理——每一步都存在概率性,模型面对同一个输入,可能给出不同的计划,可能选错工具,也可能在一个错误上反复打转。
这种不确定性本身不是洪水猛兽,真正危险的是让不确定性直接暴露给用户和业务系统,没有任何缓冲和约束。
这篇文章想解决的就是这个问题。我会拆解 Agent 类项目落地时最容易踩的五道鸿沟,从 Prompt 约束、状态管理、工具调用、质量评测到多 Agent 协作,每一道都对应具体的工程化解法,也就是很多团队在实践里沉淀下来的“系统工程化四板斧”。读完之后,你可以拿着这份清单去对照自己的项目,看它是在哪一步被卡住的。
这篇文章适合正在做 Agent 开发、Agent 架构设计,或者准备把 Agent 引入生产环境的工程师读。如果你只是单纯跑过 LangChain 的 Demo,这篇文章也能帮你提前避开后面的大坑。
2. 第一道鸿沟:Prompt 与行为之间有一条看不见的缝
2.1 很多人以为 Prompt 就是需求文档
几乎每个团队在启动 Agent 项目时,做的第一件事都是写 Prompt。然后他们会发现一个诡异的现象:Prompt 里明明写得很清楚,模型就是不按你的意思执行。
比如你写“如果用户查询订单状态,请先调用查询接口,再根据结果给出友好回复”,模型可能会在用户还没提供订单号的时候就直接调用接口,参数里塞一个猜测值,甚至把错误信息当作正常结果告诉用户。
根本原因在于:Prompt 不是需求文档。需求文档面对的是人,人可以理解语气、省略和背景;Prompt 面对的是一套统计模型,它是在做概率预测,不是在执行指令。
同一个 Prompt,换一个模型版本,行为可能就变了;同一个 Prompt 在不同日期跑,结果也可能不同。这意味着你没法把 Prompt 当作传统代码那样管理和测试。
2.2 为什么 Prompt 会失效
失效通常发生在三个层面:
第一,指令歧义。自然语言天然有歧义,模型会基于概率选择它“认为”最合理的一种解释。要降低歧义,就得把任务边界、输入输出格式、失败处理全部结构化,而不是只写一段描述。
第二,上下文干扰。模型会把上下文里所有内容都当作决策依据。如果你的系统 prompt 里混入了与任务无关的历史对话、无效搜索结果、冗余的参考资料,行为就会漂移。
第三,指令冲突。多人协作时,有人改了 Prompt 的一部分没告诉其他人,或者上层约束和下层工具说明相互矛盾,模型往往会选择最直接、最具体的那条指令,而不是优先级最高的那条。
2.3 一个典型失败案例
某团队做一个“客服工单自动分类 Agent”,Prompt 里写了“请根据用户描述判断工单类型,如果信息不足,请询问用户”。结果模型经常自作主张猜测工单类型,而不是追问。后来排查发现,Prompt 里提到了很多供应链业务的术语,而模型把这些术语当成了“分类选项”,倾向于直接匹配。这其实不是模型笨,是 Prompt 设计没有把“猜测”这条路径封死。
2.4 工程化的第一个战场:把 Prompt 变成受管制的代码
所以,Prompt 必须当作代码来管理:
- 有版本号,能回滚。
- 有测试用例,能自动验证。
- 有命名规范,能对应到具体模块。
- 有约束机制,不只是靠模型自觉,还要在代码层做硬校验。
这是“四板斧”里的第一板斧——文档与约束管理。后面会有详细方案。
3. 第二道鸿沟:无状态模型与有状态业务
3.1 LLM 天生没有状态
大模型本身是无状态的。你调用一次模型接口,它只基于你传进去的 messages 生成回复。它不记得上一次对话,不记得用户是谁,更不记得业务系统里那些还没提交的事务。
但业务是有状态的。一个订单处理 Agent,需要记录用户已经走到哪一步、订单号是什么、支付结果如何、要不要走人工审核。这些状态并不天然存在于模型里,需要工程层来管理。
很多 Agent 项目是在这一环开始失控的:状态都塞在上下文中,上下文一长就丢,或者多个环节共享一块状态,改一处崩一片。
3.2 状态管理的三个层次
从简单到复杂,Agent 状态大致分三层:
第一层,会话状态。就是当前这一轮任务里的临时信息,比如用户输入的订单号、当前正在执行的步骤。这类状态生命周期短,放在内存或上下文里就够了。
第二层,任务状态。跨多轮、跨多次模型调用的任务进度,比如一个审批流程走到第几步了。这类状态必须持久化,否则进程重启就丢了。
第三层,长期记忆。用户偏好、历史行为、领域知识,这类状态通常存库,需要时再检索出来注入上下文。很多团队会引入向量数据库来做长期记忆的检索,这没有问题,但要清楚你的记忆粒度、保留时长、权限边界分别是什么。
3.3 状态管理的一个基础示例
下面用一个最小示例演示如何把 Agent 状态剥离出来,放到独立的数据结构里管理:
# 文件路径:agent/state.py from dataclasses import dataclass, field from typing import Dict, Any, Optional import json import sqlite3 import datetime @dataclass class AgentState: """Agent 运行时状态的统一容器""" session_id: str task_type: str = "" status: str = "idle" # idle / running / waiting_input / success / failed current_step: str = "" collected_params: Dict[str, Any] = field(default_factory=dict) last_error: Optional[str] = None created_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat()) def snapshot(self) -> Dict[str, Any]: """状态快照,用于持久化或日志""" return { "session_id": self.session_id, "task_type": self.task_type, "status": self.status, "current_step": self.current_step, "collected_params": self.collected_params, "last_error": self.last_error, "created_at": self.created_at, } class StateStore: """简单的 SQLite 状态存储,生产环境可按需替换为 Redis / MySQL""" def __init__(self, db_path: str = "agent_state.db"): self.conn = sqlite3.connect(db_path) self._init_table() def _init_table(self): self.conn.execute(""" CREATE TABLE IF NOT EXISTS agent_state ( session_id TEXT PRIMARY KEY, payload TEXT NOT NULL, updated_at TEXT NOT NULL ) """) self.conn.commit() def save(self, state: AgentState): payload = json.dumps(state.snapshot(), ensure_ascii=False) self.conn.execute( "INSERT OR REPLACE INTO agent_state (session_id, payload, updated_at) VALUES (?, ?, ?)", (state.session_id, payload, datetime.datetime.now().isoformat()) ) self.conn.commit() def load(self, session_id: str) -> Optional[AgentState]: cursor = self.conn.execute( "SELECT payload FROM agent_state WHERE session_id = ?", (session_id,) ) row = cursor.fetchone() if not row: return None data = json.loads(row[0]) state = AgentState(session_id=data["session_id"]) state.task_type = data["task_type"] state.status = data["status"] state.current_step = data["current_step"] state.collected_params = data["collected_params"] state.last_error = data["last_error"] return state这段代码把状态从模型上下文中剥离出来,用独立的 StateStore 管理。Agent 每次执行关键步骤之前从 Store 加载状态,执行成功后保存新状态,这样即使中途崩溃,也能从最近一个持久化节点恢复。
这套机制的核心价值是:让 Agent 系统具备可恢复性,也就是不依赖模型上下文保存业务进度,而是由工程层掌握确定性信息。
3.4 状态工程里常见的坑
状态字段没有统一管理,散落在各个函数里;状态没有持久化,进程一重启全丢;多个 Agent 并发修改同一份状态,产生覆盖冲突;状态里存了敏感数据,又没有权限控制。这几点在后面的“四板斧”里会展开。
4. 第三道鸿沟:工具调用比想象中脆弱
4.1 Demo 能调通,真数据直接崩
Agent 的核心能力是调用工具。模型根据自己的判断,生成一个工具调用请求,系统解析请求、执行函数、把结果返回给模型。
Demo 阶段,你给模型准备的工具可能只处理“标准输入”,一切正常。但真实场景里,用户输入千奇百怪,工具返回结果也千奇百怪。常见问题包括:
- 模型编造参数,比如订单号传入一个不存在的格式。
- 工具超时,Agent 一直等待,整个流程卡死。
- 工具返回了错误信息,模型直接把错误当成正常结果告诉用户。
- 模型同一个工具反复调用,陷入无效循环。
4.2 Harness 和 Agent 到底什么关系
这里要聊一个很多人混淆的概念:Harness 和 Agent 的区别。
参考行业里比较通用的理解:Agent 是决策主体,它负责理解任务、规划步骤、决定调用哪个工具;Harness 是执行和管理 Agent 的运行时,它负责加载 Agent 配置、管理上下文窗口、调度工具执行、处理重试和终止条件。
可以这样类比:Agent 是驾驶员,Harness 是汽车本身。驾驶员决定往哪开,汽车提供油门、刹车、仪表盘、安全气囊。如果只派一个驾驶员赤手上路,出事故的概率很高。
在实际工程里,Harness 通常还要承担以下职责:
- 工具调用前的参数校验。
- 工具调用的超时控制。
- 调用失败后的重试策略。
- Agent 输出格式的强制校验。
- 终止条件的判定,防止死循环。
4.3 工具定义与参数校验示例
一个可靠的工具,不能只给模型一个函数名和描述,还要给模型明确的输入输出约束。下面这个示例展示如何定义工具,并在调用前做参数校验:
# 文件路径:agent/tools.py import json import re from typing import Dict, Any, Callable from jsonschema import validate, ValidationError def query_order(order_id: str) -> Dict[str, Any]: """模拟订单查询工具。真实项目中这里会调用后端接口。""" if not re.match(r"^ORD\d{6}$", order_id): return {"success": False, "error": "订单号格式不正确,应为 ORD 加 6 位数字"} # 模拟查询结果 return {"success": True, "order_id": order_id, "status": "已发货"} # 工具定义:模型只看到这份定义,真正的实现与定义解耦 TOOLS = { "query_order": { "description": "根据订单号查询订单状态。订单号格式为 ORD 加 6 位数字,例如 ORD123456。", "input_schema": { "type": "object", "properties": { "order_id": {"type": "string", "pattern": "^ORD\\d{6}$"} }, "required": ["order_id"] }, "handler": query_order, } } def execute_tool(tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]: """执行工具调用,带参数校验和异常捕获""" if tool_name not in TOOLS: return {"success": False, "error": f"未知工具: {tool_name}"} tool = TOOLS[tool_name] # 参数校验:模型可能编造格式,这里做硬拦截 try: validate(instance=args, schema=tool["input_schema"]) except ValidationError as e: return {"success": False, "error": f"参数校验失败: {e.message}"} # 执行工具,捕获所有异常,避免 Agent 拿到堆栈信息 try: result = tool["handler"](**args) return result except Exception as e: return {"success": False, "error": f"工具执行异常: {str(e)}"}这个示例有两个细节很关键:第一,execute_tool 对所有可能出错的地方做了兜底,模型永远不会直接看到 Python 堆栈,它只会看到一个结构化的错误信息——这样模型才能基于错误信息调整策略,而不是被一大段堆栈吓到乱猜。第二,参数校验在模型调用工具之前完成,用 JSON Schema 硬约束参数格式,模型没按格式来就直接拒绝,不会把一个错误请求发到业务系统。
4.4 重试与超时:防止 Agent 卡死
Agent 工具调用还需要一个执行器,负责超时和重试:
# 文件路径:agent/executor.py import time from typing import Dict, Any def call_tool_with_policy(tool_name: str, args: Dict[str, Any], max_retries: int = 2, timeout_seconds: int = 10) -> Dict[str, Any]: last_error = "" for attempt in range(max_retries + 1): start = time.time() try: result = execute_tool(tool_name, args) if result.get("success"): return result last_error = result.get("error", "未知错误") except Exception as e: last_error = str(e) finally: cost = time.time() - start if attempt < max_retries: time.sleep(1) # 重试前短暂等待 return {"success": False, "error": f"工具调用失败(重试 {max_retries} 次),最后一次错误: {last_error}"}这个执行器做的事情在 Demo 里经常被忽略,但它直接影响 Agent 在生产环境能不能稳定工作。没有超时控制,一个工具接口挂了,整个 Agent 就挂在那里等;没有重试策略,偶发的网络抖动会让一次本来能成功的任务率直接失败。
4.5 工具面的工程判断
工具面真正难的不是把函数暴露给模型,而是把工具的边界定义清楚。哪些参数允许模型自由生成,哪些参数必须由上游系统注入而不能让模型编造(比如用户身份、权限 token),这些要在工具协议层定死。这也是后面第三板斧要展开的重点。
5. 第四道鸿沟:质量不稳定,上线无法安心
5.1 传统测试方法失效了
传统软件测试的核心是断言:输入一个固定值,断言输出等于期望值。Agent 系统做不到这一点,因为模型输出具备概率性,同一个 Prompt 跑两次,结果可能不完全一样。
于是很多团队面临一个尴尬:Agent 在演示时表现很好,你很难说它“做错了”;但把它放到线上,又不敢保证它每次都不犯错。这种不确定性带来一个强烈的需求:必须给 Agent 系统建立一套质量评估和监控机制。
5.2 从四个层次建立评测
在实践中,Agent 的质量评测通常分四层:
第一层,规则断言。检查输出里是否包含关键字段、是否满足 JSON 格式、是否调用了应该调用的工具。这一层最便宜、最可自动化。
第二层,数据集评测。准备一批固定的测试输入,每个输入标注期望行为(比如期望调用哪个工具、期望最终状态是什么),定期跑一遍,看通过率变化。发布新版本 Prompt 之前,先跑这批用例。
第三层,模型作为裁判(LLM-as-judge)。对于无法写死的开放任务,用更强的模型评估输出的质量,给分并给出理由。这一层要注意裁判模型的偏差,建议同时抽查人工标注。
第四层,线上监控与影子模式。新版本先只记录不实际执行业务动作,对比它与旧版本的差异;上线后记录每一次决策轨迹,出现异常可以回滚。
5.3 评测脚本示例
下面展示一个最基本的评测脚本,用来在每次修改 Prompt 后自动验证 Agent 的输出:
# 文件路径:tests/evaluate_agent.py import json from agent.state import AgentState, StateStore from agent.tools import execute_tool def run_test_case(state: AgentState, user_input: str) -> dict: """模拟一个最简单的 Agent 执行流程,返回执行结果和调用日志""" call_log = [] # 简化流程:先判断是否需要查询订单 if "查" in user_input and "订单" in user_input: # 从用户输入里提取订单号(简化示例,真实项目用正则或模型抽取) order_id = extract_order_id(user_input) tool_result = execute_tool("query_order", {"order_id": order_id}) call_log.append({"tool": "query_order", "args": {"order_id": order_id}, "result": tool_result}) if tool_result["success"]: state.status = "success" state.current_step = "order_queried" else: state.status = "waiting_input" state.last_error = tool_result["error"] else: state.status = "waiting_input" return {"state": state.snapshot(), "call_log": call_log} def extract_order_id(text: str) -> str: """简化实现,真实项目中建议用正则或模型抽取""" import re match = re.search(r"ORD\d{6}", text) return match.group(0) if match else "invalid" def evaluate(): test_cases = [ {"input": "帮我查一下订单 ORD123456 的状态", "expect_status": "success", "expect_tool": "query_order"}, {"input": "你好,你是谁?", "expect_status": "waiting_input", "expect_tool": None}, {"input": "帮我查一下订单 12345 的状态", "expect_status": "waiting_input", "expect_tool": "query_order"}, ] store = StateStore(":memory:") passed = 0 total = len(test_cases) for case in test_cases: state = AgentState(session_id="test") result = run_test_case(state, case["input"]) state_snapshot = result["state"] ok = state_snapshot["status"] == case["expect_status"] if case["expect_tool"]: tools_called = [log["tool"] for log in result["call_log"]] ok = ok and (case["expect_tool"] in tools_called) if ok: passed += 1 print(f"[PASS] {case['input']}") else: print(f"[FAIL] {case['input']}") print(f" 期望: status={case['expect_status']}, tool={case['expect_tool']}") print(f" 实际: {state_snapshot}") print(f"\n评测结果: {passed}/{total} 通过") return passed == total if __name__ == "__main__": evaluate()如果你在修改 Agent 的 Prompt 或者工具定义后,先跑一遍评测脚本,再决定是否上生产环境,整个项目的稳定性会高很多。
5.4 质量问题的数据判断
很多团队觉得 Agent 项目“不好评估”,于是干脆不评估,靠“跑一下看看”来碰运气。实际项目里,建议从最少量的规则断言开始,逐步积累测试用例。哪怕只有 10 个核心用例,也比完全盲跑强,因为你至少能抓住“改了一版 Prompt 后,所有功能是不是还正常”这样的大问题。
6. 第五道鸿沟:多 Agent 协作与安全边界
6.1 任务复杂了,一个 Agent 扛不住
当业务任务足够复杂,一个 Agent 处理不了时,很多团队会自然走向多 Agent 协作:规划 Agent 拆任务,执行 Agent 干活,审核 Agent 验收结果。
但多 Agent 协作会引入新的问题。第一个是死循环:两个 Agent 之间反复确认,谁都不往前走。第二个是踢皮球:每个 Agent 都认为任务不归自己管,结果任务悬空。第三个是上下文污染:A Agent 的中间结果被塞给 B Agent,却没有做必要的清洗和隔离。
实际运行时,你可能在日志里看到类似的报错:Agent terminated due to error。这类错误往往不是模型本身的问题,而是多 Agent 协作流程缺少终止条件和边界约束,导致任务在一个错误路径上反复执行,最后被运行时强制终止。
6.2 多 Agent 协作的工程化设计
多 Agent 协作要稳定,必须做三件事:
第一,明确任务所有权。每个 Agent 只负责一类任务,接收什么输入、输出什么结果、遇到什么情况上报或终止,都要在配置里写清楚,而不是让模型自由发挥。
第二,设置终止条件。每一个子任务都要有最大迭代次数限制和超时时间。一旦超过阈值,立即进入降级流程,比如转交人工处理,不能无限重试。
第三,隔离上下文。多 Agent 之间通过消息传递数据,而不是共享一个大而全的上下文。这样既能避免上下文超长,也能防止一个 Agent 的错误信息污染另一个 Agent 的决策。
6.3 一个多 Agent 协作的配置示例
# 文件路径:config/multi_agent.yaml version: "1.0" project: "order-handling-system" agents: - name: "router" role: "意图路由" model: "gpt-4o" max_iterations: 3 timeout_seconds: 30 description: "分析用户输入,判断任务应转发给哪个下游 Agent" tools: - "classify_intent" downstream: - "order_query" - "order_refund" - name: "order_query" role: "订单查询" model: "gpt-4o" max_iterations: 5 timeout_seconds: 60 description: "负责订单状态查询,必须使用 query_order 工具,不得猜测订单号" tools: - "query_order" constraints: - "order_id 必须符合 ORD 加 6 位数字格式" - "查询失败时,必须向用户询问正确的订单号,禁止编造查询结果" on_error: "notify_human" - name: "order_refund" role: "退款处理" model: "gpt-4o" max_iterations: 8 timeout_seconds: 120 description: "负责退款申请,需要调用退款接口并通知用户结果" tools: - "create_refund" - "notify_user" constraints: - "退款前必须确认用户身份和订单归属" - "退款金额超过 1000 元时,必须转人工审核" on_error: "escalate_manual" global_policies: max_total_iterations: 30 max_total_timeout_seconds: 300 log_level: "DEBUG" audit_enabled: true fallback_agent: "human_service"这个配置里值得关注的是 constraints 和 max_iterations。constraints 是开发者站在工程视角给 Agent 划定的安全边界,max_iterations 则是硬性的终止条件。有了这些配置,即使发生“Agent terminated due to error”,系统也能在预期范围内终止,而不是无限空转。
6.4 Agent 安全边界与最小权限
多 Agent 协作还涉及一个很容易被忽视的问题:权限控制。在传统后端系统里,我们知道要给不同服务分配不同的数据库账号,遵循最小权限原则。但到了 Agent 场景,很多团队反而忘了这一点。
一个 Agent 系统里接入的工具,权限范围应该严格限定在它的任务范围内。查询 Agent 只拥有查询接口的权限,退款 Agent 才拥有退款接口的权限。凭证不能写死在代码里,应该通过环境变量或密钥管理服务注入。
此外,所有 Agent 的关键操作都要记录审计日志——谁在什么时间调用了什么工具,传了什么参数,返回了什么结果。这样一旦出现问题,可以回溯定位。
6.5 Agent 安全的核心判断
多 Agent 系统比单 Agent 系统多出来的复杂度,不只是模型层面的,更是工程层面的任务编排、终止策略和权限隔离。如果这三个问题没有在设计阶段想清楚,上线的 Agent 越多,系统越不稳定。
7. 系统工程化四板斧:把 Agent 从 Demo 推向生产
前面五章讲了五道鸿沟:Prompt 与行为之间的鸿沟、无状态模型与有状态业务的鸿沟、工具调用脆弱性的鸿沟、质量不可控的鸿沟、多 Agent 协作和安全边界的鸿沟。每一道鸿沟背后都对应一个工程化措施,综合起来,就是很多团队总结出来的“系统工程化四板斧”。
7.1 第一板斧:文档与约束管理
文档与约束管理的核心想法是:与其依赖模型“听懂”你的要求,不如把要求结构化、可校验、版本化,让系统从机制上保证 Agent 的行为不越界。
具体实践包括:
- 为每个 Agent 建立独立的配置目录,包含 Prompt、工具协议、约束规则、评测用例。
- Prompt 文件纳入 Git 管理,每次修改都走评审和测试流程。
- 用 YAML 或 JSON 定义 Agent 的约束规则,禁止模型在什么条件下执行什么动作。
- 对 Prompt 的输出做 Schema 校验,不符合格式就拒绝,而不是交给下游继续处理。
一个单 Agent 的配置目录结构示例:
my-agent/ ├── config.yaml # Agent 基础配置 ├── prompt/ │ ├── system.md # 系统提示词 │ └── output_schema.json # 输出格式约束 ├── tools/ │ ├── tool_defs.yaml # 工具协议定义 │ └── handlers/ # 工具实现代码 ├── tests/ │ ├── cases.json # 离线评测用例 │ └── evaluate.py # 评测脚本 └── logs/ └── audit/ # 审计日志这种目录结构和传统后端项目的分层很相似,它让 Agent 项目具备了可读性、可维护性和可协作性。对于一个团队来说,这比任何华丽的 Demo 都重要。
7.2 第二板斧:状态与记忆工程
状态与记忆工程的核心想法是:把 Agent 的确定性信息和不确定性信息分开管理,确定性的业务状态交还给数据库,不确定性的推理过程才交给模型。
实践中需要落地这几点:
- 统一的状态容器,覆盖会话状态、任务状态和长期记忆。
- 状态持久化,关键节点必须落库,进程重启可恢复。
- 记忆分层:短期记忆放会话上下文,长期记忆进向量库或数据库。
- 状态访问权限控制,敏感数据不能出现在模型上下文中。
记忆分层的典型配置:
memory: short_term: type: "context_window" max_tokens: 8000 strategy: "sliding_window" long_term: type: "vector_store" collection: "user_preferences" embedding_model: "text-embedding-3-small" retrieval: top_k: 5 similarity_threshold: 0.7这里要特别提醒:不要把用户的身份证号、密码、支付 token 之类的敏感数据注入长期记忆,也不要为了上下文方便就把这些数据塞给模型。记忆系统必须和权限系统联动。
7.3 第三板斧:工具与 Harness 规范化
工具与 Harness 规范化的核心想法是:工具不是“模型可以调用的函数列表”,而是需要协议化管理的服务接口。模型只是在协议范围内生成工具调用请求,真正执行和兜底的是 Harness 运行时。
落地建议:
- 所有工具必须有输入输出 Schema,参数校验在前置层完成。
- 所有工具调用必须有超时和重试策略。
- 工具返回结果统一为结构化格式(success + data / error)。
- Harness 负责上下文窗口管理、调用历史截断、终止判定。
- 工具执行权限和凭证由 Harness 注入,模型看不到敏感凭证。
前面第四节已经给了工具定义和执行器的示例,这里不再重复代码,但有一点值得强调:把工具执行从模型逻辑中完全独立出来,是 Agent 系统走向生产环境的分水岭。如果你现在还是让模型直接调用 Python 函数,还没有任何超时和参数校验,那你其实还停留在 Demo 阶段。
7.4 第四板斧:可观测性与评测
可观测性与评测的核心想法是:Agent 系统的每一项关键行为都必须可以追踪、可以度量、可以回放。如果你想在上线前发现问题、在上线后快速定位问题,就必须把观测和评测当作基础设施来做,而不是事后补丁。
落地清单:
- 给每一次 Agent 运行生成唯一的 trace_id,贯穿整条链路。
- 记录每一步的模型输入输出、工具调用、参数、结果、耗时。
- 日志中保留原始输入,方便问题回放。
- 建立离线评测用例集,每次 Prompt 或工具变更都跑一遍。
- 灰度发布,先用少量流量验证再全量。
一个可观测日志的字段建议:
| 字段 | 示例 | 说明 |
|---|---|---|
| trace_id | 88f4a1c2-9f3e-4b6a | 一次完整任务的唯一标识 |
| agent_name | order_query | 当前 Agent 名称 |
| tool_name | query_order | 调用的工具名 |
| input_args | {"order_id": "ORD123456"} | 工具输入参数 |
| output_data | {"success": true, "status": "已发货"} | 工具返回结果 |
| duration_ms | 120 | 调用耗时 |
| error | null | 错误信息,无则为 null |
| timestamp | 2026-08-12T14:30:22 | 操作时间 |
8. 常见问题与排查思路
在接入工程化方案的过程中,很可能会遇到下面这些问题,这里整理成排查清单:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Agent 执行卡死,长时间无响应 | 工具调用没有超时限制,接口 hang 住 | 查看运行日志中最后一个工具调用的开始时间 | 为所有工具调用增加超时控制,超时后走降级逻辑 |
| Agent 反复调用同一个工具,陷入死循环 | 缺少最大迭代次数限制 | 检查日志中工具调用次数是否显著异常 | 在 Harness 层设置 max_iterations,终止后转人工或返回兜底话术 |
| 多 Agent 互相等待,任务不推进 | 下游 Agent 的输入缺少必要参数,一直等待重新注入 | 查看各 Agent 的状态机流转是否卡在 waiting 状态 | 为每个任务设置全局超时时间,超时自动转人工 |
| 修改 Prompt 后,行为反而变差 | 缺少 Prompt 版本管理和评测用例 | 对比新旧版本在同一评测集上的通过率 | 每次改动 Prompt 前跑离线评测,通过后才允许上线 |
| 模型生成了不存在或错误的订单号 | 输入缺少格式约束,校验前置层缺失 | 检查工具调用参数是否通过 Schema 校验 | 增加工具参数 JSON Schema 校验,格式不合法直接拦截 |
| Agent 把错误信息当成成功结果告诉用户 | 工具返回结构不统一,模型无法区分成功与失败 | 查看工具返回结果是否包含 success 字段 | 统一工具返回结构,Harness 在返回给模型前先做语义化封装 |
| 上下文越来越长,响应变慢 | 没有做上下文窗口管理,历史全部堆在 messages 里 | 查看单次请求的 token 消耗 | 设置上下文压缩策略,滑动窗口淘汰不重要的历史消息 |
| 用户敏感数据出现在模型上下文中 | 缺少状态访问控制,数据未脱敏 | 审计日志中排查模型输入是否包含敏感字段 | 在注入上下文之前做数据脱敏和权限校验 |
| Agent terminated due to error | 任务运行过程中错误累积超过终止阈值 | 查看终止前的错误日志序列 | 优化异常处理策略,限制重试次数,终止后转人工处理 |
9. 最佳实践与工程建议
9.1 命名规范
Agent 项目里会有大量的 Prompt、工具、状态字段、测试用例,命名不统一会让项目迅速失控。建议从一开始就约定规范:
- Agent 名称用“业务域 + 职责”,例如 order_query、refund_handler。
- 工具名称统一用动词开头,例如 query_order、create_refund。
- 状态字段统一用 snake_case,布尔型用 is_ 前缀。
- 测试用例的命名包含场景和期望结果,例如 test_order_query_success。
9.2 配置管理
Agent 的配置和代码一样需要版本化管理。Prompt、工具定义、Agent 编排配置都应该入库,并且用标签区分开发版、预发版、生产版。不要让团队成员在本地随意修改生产配置然后重新部署,每次配置变更要走评审和测试流程。
9.3 错误处理与降级
Agent 系统一定会出错,所以错误处理策略比错误预防更重要。实践上建议给每个 Agent 定义清晰的降级路径:出错时优先重试一次,重试无效就转人工,而不是把错误信息直接抛给用户。特别注意区分“模型可恢复的错误”(比如信息不足、参数格式不对)和“模型不可恢复的错误”(比如工具接口 500、认证失败),前者让模型继续调整,后者立即终止并告警。
9.4 审计与安全边界
Agent 能调用的每项工具,都要有明确的权限边界。按最小权限原则分配凭证,按需要脱敏数据,按风险等级设置人工审批节点。涉及资金、退款、删除等高风险操作时,不要让 Agent 直接执行,先转人工审核。所有操作落审计日志,这是事故复盘和合规审计的基础。
9.5 灰度与回滚
不要一次性把所有流量切到新版本 Agent 上。建议做法是:先在测试流量上验证新版本行为,再逐步放大到 5%、20%、50%、100%。一旦发现异常,立即回滚到上一个稳定版本。Agent 系统和传统系统一样,需要灰度发布工具和回滚机制,否则一次版本更新就可能造成大面积线上问题。
9.6 团队协作
Agent 项目不是一个模型工程师能独立扛下来的。一个稳定的 Agent 产品团队,至少需要有:
- 负责 Prompt 和评测的算法工程师。
- 负责工具接入和后端服务的后端工程师。
- 负责数据安全、权限和审计的安全工程师。
- 负责整体架构和流水线的平台工程师。
分工越清楚,Agent 项目的工程质量越高。
10. 从五道鸿沟到四板斧,Agent 工程化的本质
回到开头的问题:约四成 Agent 项目失败,原因到底是什么?
核心不是模型能力不够,而是工程化没有跟上。很多人把 Agent 当成“一个更聪明的 API 调用”,写完 Prompt 就等着模型自己把活干好。结果 Prompt 不可控、状态丢失、工具调用崩溃、线上无法观测、多 Agent 相互干扰,最终项目在真实业务面前撑不住。
五道鸿沟本质上说的是同一件事:模型负责生成可能性,工程负责收敛可能性。Prompt 和约束在收敛行为,状态和记忆在收敛时序,工具协议在收敛外部依赖,评测和观测在收敛质量,多 Agent 编排和权限在收敛协作边界。这五道收敛缺任何一道,Agent 都只是试验品,不是产品。
系统工程化四板斧正好回答了“怎么收敛”的问题:
- 文档与约束管理:让 Agent 的行为可定义。
- 状态与记忆工程:让 Agent 的运行可恢复。
- 工具与 Harness 规范化:让 Agent 的行动受控。
- 可观测性与评测:让 Agent 的质量可信。
如果你正在启动一个 Agent 项目,建议从最小闭环开始:先让一个 Agent 完成一个核心任务,把这个任务的 Prompt、工具、状态、评测、日志全部配好,跑通第一条用户请求的完整链路。然后再逐步扩展任务类型、接入多 Agent 协作、增加更复杂的编排逻辑。每一步都回头检查四板斧是否覆盖到位。
从更长期的视角看,Agent 开发正在从拼模型、拼 Prompt,转向拼架构、拼稳定性、拼安全合规。那些能把 Agent 工程化的团队,会在这轮技术周期里积累起真正的护城河。如果你的团队还在为 Agent 项目的稳定性发愁,可以先拿着这份清单做一次项目体检,找到最薄弱的那道鸿沟,优先补上。建议收藏备用,也可以转发给正在做 Agent 开发的同事,一起减少“四成失败率”。
