1. 从工具调用到协作伙伴:Agent范式的本质变化
过去两年,我参与过不少Agent相关的项目,从最早的"给LLM挂几个API"到后来做完整的任务编排系统,踩过的坑比写过的代码还多。这篇总结想聊的核心,是一个我反复在项目里验证过的判断:Agent正在从"工具调用器"变成"协作伙伴",而这个转变的关键,不在于模型本身有多强,而在于Harness这一层工程做得好不好。
先把几个概念摆清楚,因为热词里混着"harness和agent区别""agent框架""llm框架"这些搜索,说明很多人对边界是模糊的。我的理解是:LLM是大脑,Tool是手脚,Skill是肌肉记忆,而Harness是把这些串起来、让Agent能稳定跑完一个长任务的"骨架+神经系统"。Agent则是最终呈现出来的那个能自主决策、能纠错、能和人协作的整体。这四者不是并列关系,是层层包裹的关系。
为什么说这是"范式跃迁"而不是"版本升级"?因为工具调用时代的核心指标是"单次调用成功率",而Agent时代的核心指标变成了"多轮任务完成率"。前者你只要把参数拼对就行,后者你要考虑上下文怎么管理、失败怎么回滚、记忆怎么沉淀、多个Skill怎么协同。这是两个完全不同的工程问题。
我见过太多团队,模型选的是顶配,Tool接了几十个,结果跑一个稍微复杂点的任务就崩,问题几乎都出在Harness层——没有状态管理、没有错误恢复、没有Skill之间的调度逻辑。所以这篇总结的重点会放在工程实现上,而不是模型选型上。
2. 核心概念拆解:LLM、Tool、Skill、Harness到底怎么分工
2.1 LLM的角色:不是万能大脑,而是"决策中枢"
很多人对LLM在Agent里的定位有误解,觉得它应该什么都懂、什么都能干。实际项目里,LLM最擅长的是意图理解、任务分解、工具选择这三件事,而不是执行具体操作。
我常用的一个类比:LLM就像一个刚入职的聪明项目经理,他知道大概要做什么,但具体每个环节怎么落地,他需要问专家(Tool)、查手册(知识库)、或者调用现成的流程(Skill)。你让他直接去写一个复杂的SQL,他可能写错;但你让他判断"这个需求该不该查数据库、该查哪张表",他判断得比规则引擎准得多。
这里有个关键点:LLM的token机制决定了它的能力边界。热词里有个很有意思的说法——"key我是谁、query我在找什么、value我能提供什么",这其实就是注意力机制的本质。LLM在处理长上下文时,会不自觉地给不同信息分配不同的注意力权重。这意味着,如果你把Tool的描述、Skill的说明、历史对话全塞进prompt里,LLM很可能"看漏"关键信息。
我的做法是:给LLM的上下文做分层。系统指令(我是谁)放最前面,当前任务(我在找什么)放中间,可用的Tool和Skill列表(我能提供什么)放最后,并且用清晰的分隔符隔开。实测下来,这样能让工具选择的准确率提升不少。
2.2 Tool与Skill的区别:一个是原子操作,一个是组合拳
这两个概念经常被混用,但在工程上必须分清。
Tool是原子操作,比如"查询天气""发送邮件""读取文件"。它是最小的执行单元,输入输出都很明确,通常对应一个API或者一个函数。
Skill是组合能力,比如"处理客户投诉"这个Skill,内部可能包含"查询订单Tool→判断问题类型→调用退款Tool→发送安抚邮件Tool"这一整套流程。Skill是有状态的、有分支逻辑的、可以复用的。
热词里"skill编码247""skill编码193""skill插件""codex skill"这些搜索,说明大家已经在关注Skill的标准化问题了。我的经验是:Skill一定要有明确的输入契约和输出契约,否则多个Skill串联时会出现"上一个Skill的输出格式,下一个Skill读不懂"的问题。
举个我实际踩过的坑:早期我们做了一个"生成周报"的Skill,输出是纯文本;后来又做了一个"发送周报"的Skill,期望输入是结构化的JSON。结果两个Skill串起来直接报错。后来我们定了个规矩:所有Skill的中间产物必须是结构化数据,只有最终面向用户的输出才转成自然语言。
2.3 Harness:被严重低估的"工程骨架"
Harness这个词在热词里出现频率极高——"deepseek harness""harness工程""harness engineering""harness anything"。但很多人还是把它理解成一个"调度器",这就太小看它了。
我的定义是:Harness是Agent的运行时环境,负责状态管理、上下文组装、错误处理、Skill调度、记忆读写、安全边界控制。它不参与具体决策,但决定了Agent能不能稳定跑完一个长任务。
打个比方:LLM是司机,Tool是车上的各种按钮,Skill是驾驶技巧,Harness就是整辆车的底盘、电路、油路系统。司机再厉害,底盘散了也开不动。
Harness要解决的核心问题有三个:
- 状态持久化:Agent跑一个任务可能要几十轮,中间如果进程重启,状态不能丢。
- 上下文窗口管理:LLM的上下文有限,Harness要决定哪些历史信息保留、哪些压缩、哪些丢弃。
- 失败恢复:某个Tool调用失败了,是重试、换方案、还是上报给用户?这个决策逻辑在Harness里。
我见过一个团队,Agent跑得好好的,结果服务器一重启,所有进行中的任务全丢了,因为状态全在内存里。这就是Harness没做好的典型表现。
3. 工业界实战:一个Agent系统的完整搭建过程
3.1 需求拆解:先想清楚"谁用、干什么、干到什么程度"
任何Agent项目,第一步都不是写代码,而是把需求拆清楚。我习惯用三个问题来框定范围:
- 谁用:是内部员工用,还是终端用户用?这决定了容错率。内部工具可以容忍偶尔报错,面向用户的必须做到"优雅降级"。
- 干什么:是单轮问答,还是多轮任务?是确定性流程,还是需要自主决策?这决定了Harness的复杂度。
- 干到什么程度:是辅助人做决策,还是直接替人执行?这决定了安全边界。
我做过一个"合同审核Agent",需求是辅助法务人员快速定位风险条款。这个场景下,Agent不需要做最终决策,只需要把可疑条款标出来并给出理由。所以我们的Harness设计得很轻——不需要复杂的回滚机制,因为Agent不会真的修改合同。
但后来做"自动退款Agent"时,情况完全不同。这个Agent会真的调用退款接口,一旦出错就是真金白银的损失。所以Harness里加了大量的校验层:金额超过阈值要人工确认、同一用户短时间内多次退款要拦截、退款前必须二次查询订单状态。
提示:需求阶段一定要把"Agent能做什么"和"Agent绝对不能做什么"写清楚,后者往往比前者更重要。
3.2 工具层设计:Tool的粒度怎么把握
Tool的粒度是个很微妙的问题。太粗,LLM不好选;太细,LLM要调很多次,token消耗大且容易出错。
我的经验法则是:一个Tool应该对应一个"人类会一次性完成的动作"。比如"查询订单"是一个Tool,但"查询订单并计算退款金额"就不该是一个Tool,因为后者包含了两步逻辑,应该拆开。
具体到实现,我通常会给每个Tool定义这几个字段:
{ "name": "query_order", "description": "根据订单号查询订单详情,返回订单状态、金额、下单时间", "parameters": { "order_id": { "type": "string", "description": "订单号,通常是16位数字", "required": true } }, "returns": { "status": "订单状态,枚举值:pending/paid/shipped/completed/cancelled", "amount": "订单金额,单位元", "created_at": "下单时间,ISO8601格式" } }注意description和returns这两个字段。很多团队只写parameters,结果LLM不知道这个Tool返回什么,就不会在合适的时机调用它。Tool的description要写"什么时候用",returns要写"用了能得到什么",这两句话直接决定了LLM的选择准确率。
还有个细节:参数描述里要给出格式示例。"订单号,通常是16位数字"比"订单号"要好得多,因为LLM会据此判断用户输入是否合法。
3.3 Skill编排:把多个Tool串成一条稳定的流水线
Skill的本质是预定义的任务流程。它和"让LLM自由发挥"是两种不同的思路,各有适用场景。
- LLM自由发挥:适合开放式任务,比如"帮我分析这份报告"。优点是灵活,缺点是结果不稳定。
- Skill编排:适合确定性任务,比如"处理退款申请"。优点是稳定可复现,缺点是遇到流程外的情况就卡住。
我的做法是混合使用:主流程用Skill编排保证稳定性,每个Skill内部的判断节点让LLM来做,遇到Skill覆盖不了的情况,再交给LLM自由发挥。
一个退款Skill的伪代码大概长这样:
def refund_skill(order_id, reason): # 第一步:查询订单 order = call_tool("query_order", order_id=order_id) if order.status == "cancelled": return {"result": "fail", "msg": "订单已取消,无需退款"} # 第二步:LLM判断是否符合退款条件 judgment = llm_decide( context=f"订单状态:{order.status},金额:{order.amount},退款原因:{reason}", question="是否符合退款条件?返回yes或no及理由" ) if judgment.answer == "no": return {"result": "reject", "msg": judgment.reason} # 第三步:执行退款 refund_result = call_tool("execute_refund", order_id=order_id, amount=order.amount) # 第四步:通知用户 call_tool("send_notification", user_id=order.user_id, content="退款已处理") return {"result": "success", "refund_id": refund_result.id}这个Skill里,第一步和第三步是确定性的Tool调用,第二步是LLM判断。这样既保证了流程稳定,又保留了灵活性。
注意:Skill里的LLM判断节点一定要设置超时和兜底逻辑。我遇到过LLM返回格式不对导致整个Skill卡死的情况,后来加了"如果LLM返回无法解析,默认走人工审核"的兜底。
3.4 Harness实现:状态、上下文、错误处理三件套
Harness是整篇文章的重点,我拆成三块来讲。
状态管理:Agent的每一轮对话、每一次Tool调用、每一个中间结果,都要持久化。我用的是"事件溯源"的思路——不存最终状态,而是存所有发生的事件,需要时重放。这样即使进程崩溃,重启后重放事件就能恢复。
存储选型上,轻量场景用SQLite就够了,重场景上PostgreSQL。关键是每个事件要有唯一ID和时间戳,方便排查问题。
上下文组装:这是Harness里最考验工程能力的地方。LLM的上下文窗口有限,你不能把所有历史都塞进去。我的策略是分层压缩:
- 最近3轮对话:完整保留,一字不改。
- 3-10轮之前的对话:只保留"用户意图+Agent动作+结果摘要"。
- 10轮之前:只保留关键结论,比如"用户已确认退款金额"。
这样能把上下文控制在合理范围内,同时不丢失关键信息。
错误处理:Tool调用失败是常态,Harness要有一套完整的处理策略。我总结了一个决策树:
| 错误类型 | 处理策略 | 重试次数 |
|---|---|---|
| 网络超时 | 自动重试 | 3次,指数退避 |
| 参数错误 | 让LLM修正参数后重试 | 2次 |
| 权限不足 | 上报用户,请求授权 | 0次 |
| 业务规则拒绝 | 直接返回失败原因 | 0次 |
| 未知错误 | 记录日志,降级处理 | 1次 |
这张表是我们踩了无数坑之后总结出来的。比如"参数错误"让LLM修正,是因为很多时候是LLM自己拼错了参数格式,让它看一眼错误信息就能改对。而"权限不足"绝对不能自动重试,否则会触发风控。
4. 常见问题与排查技巧实录
4.1 Agent"跑偏"了怎么办:上下文污染的排查
最常见的现象是:Agent跑着跑着,突然开始做和任务无关的事情。这通常是上下文污染导致的。
排查思路:
- 打印完整的上下文,看有没有无关信息混进去。
- 检查Tool的返回值,有没有把整个数据库记录都塞进去了。
- 检查Skill的中间产物,有没有把调试信息也传下去了。
我遇到过一个案例:Agent在查订单时,Tool返回了完整的订单对象,包含了几十个字段,其中有个字段叫internal_notes,里面是客服的内部备注。LLM看到这些备注后,开始"脑补"用户的情绪,然后自作主张地改变了回复策略。后来我们在Tool层做了字段过滤,只返回必要的字段,问题就解决了。
提示:Tool的返回值一定要做白名单过滤,不要图省事直接返回整个对象。
4.2 Skill串联时的"格式不兼容"问题
前面提过,多个Skill串联时,格式不兼容是高频问题。我的解决方案是定义统一的中间数据格式。
我们内部定了一个叫AgentMessage的结构:
{ "type": "tool_result | llm_output | user_input", "source": "skill_name or tool_name", "timestamp": "2024-01-01T00:00:00Z", "payload": {}, "meta": { "confidence": 0.95, "need_human_review": false } }所有Skill的输入输出都用这个结构包裹。这样不管上游是哪个Skill,下游都能统一解析。meta字段里的confidence和need_human_review特别有用——当LLM判断的置信度低于阈值时,自动触发人工审核。
4.3 并发场景下的状态冲突
热词里有个"ai agent 怎么扛并发",这是个真问题。多个用户同时用Agent,如果状态管理没做好,会出现A用户的操作影响了B用户的任务。
我的做法是每个会话独立的状态空间。会话ID作为命名空间,所有状态读写都带上会话ID。数据库层面用session_id做分区键,避免跨会话查询。
另外,对于共享资源(比如同一个订单被两个Agent同时操作),要加乐观锁。具体做法是:读取时记录版本号,写入时检查版本号是否变化,变了就重试。
UPDATE orders SET status = 'refunding', version = version + 1 WHERE order_id = ? AND version = ?如果影响行数为0,说明版本号变了,说明有别的Agent在操作,这时候要重新读取最新状态再决策。
4.4 LLM"幻觉"导致Tool调用错误
LLM会编造不存在的参数值,这是幻觉的典型表现。比如用户说"帮我查一下昨天的订单",LLM可能编一个订单号出来。
防御手段有三层:
- 参数校验:Tool层对参数做格式校验,订单号必须是16位数字,不符合直接拒绝。
- Prompt约束:在系统指令里明确写"如果用户没有提供必要参数,必须先询问用户,不得编造"。
- 二次确认:对于关键操作(如退款),执行前让LLM复述一遍参数,确认无误再执行。
第三层特别有效。我们让LLM在调用退款Tool前,先生成一句"我将为订单XXX退款YYY元,请确认",用户确认后才真正执行。这个简单的机制拦住了不少幻觉导致的错误。
4.5 排查速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent不调用Tool | Tool描述不清 | 打印Tool列表看LLM是否理解 | 优化description,加使用场景 |
| 调用错误的Tool | Tool之间描述重叠 | 对比相似Tool的描述 | 合并或明确区分边界 |
| 参数格式错误 | 缺少格式示例 | 看LLM生成的参数 | 在参数描述里加示例 |
| 任务中途卡死 | 缺少超时机制 | 看日志最后一步 | 加超时和兜底逻辑 |
| 结果不稳定 | 上下文污染 | 打印完整上下文 | 做上下文分层和过滤 |
| 并发冲突 | 状态未隔离 | 检查session_id | 加会话隔离和乐观锁 |
5. 从工具到伙伴:Agent演进的几个观察
5.1 记忆机制是"伙伴感"的来源
工具和伙伴的区别是什么?工具用完就忘,伙伴会记得你上次说过什么。
Agent要产生"伙伴感",必须有长期记忆。这个记忆不是简单的对话历史,而是结构化的用户画像和偏好。比如"这个用户喜欢简洁的回复""这个用户上次退款是因为物流太慢"。
实现上,我用的是"记忆提取+记忆检索"两段式。每轮对话结束后,让LLM提取值得记住的信息,存到向量数据库;下一轮对话开始时,根据当前任务检索相关记忆,注入上下文。
关键是记忆要有衰减机制。不是所有记忆都永久保留,时间久远的、重要性低的记忆要逐渐淡化,否则记忆库会越来越臃肿,检索质量下降。
5.2 Skill的复用与组合是规模化的关键
单个Agent能做的事有限,真正有价值的是Skill的复用。我们内部建了一个Skill库,每个Skill都有标准化的输入输出契约,新项目可以直接引用。
比如"发送通知"这个Skill,在退款Agent、客服Agent、营销Agent里都能用。这样新项目启动时,不用从零开始,直接组合现有Skill就行。
热词里"book to skill""workbuddy skill"这些搜索,说明大家已经在探索Skill的标准化和复用问题了。我的建议是:从第一个项目开始就按标准格式写Skill,哪怕当时只有你一个人用。等到Skill多了,标准化带来的收益会非常明显。
5.3 安全边界:Agent越强,边界越重要
Agent能力越强,越需要明确的安全边界。我的原则是**"最小权限+关键操作人工确认"**。
最小权限是指:Agent只能访问完成任务必需的资源。查订单的Agent不应该有修改用户的权限,发邮件的Agent不应该有读取财务数据的权限。
关键操作人工确认是指:涉及资金、隐私、不可逆操作时,必须有人工确认环节。这个确认不是简单的"是/否",而是要让用户看到Agent的决策依据,比如"我将退款100元,因为订单状态是已支付且用户提供了合理的退款理由"。
注意:安全边界不是限制Agent能力,而是让Agent的能力可以被信任地使用。没有边界的Agent,没人敢用。
5.4 评估体系:怎么知道Agent做得好不好
Agent的评估比传统软件难得多,因为它的输出不是确定性的。我的做法是分层评估:
- Tool层:调用成功率、平均耗时、错误率。这些是硬指标,和传统API监控一样。
- Skill层:任务完成率、平均轮次、人工介入率。这些反映流程的稳定性。
- Agent层:用户满意度、任务一次通过率、异常处理能力。这些需要人工标注或用户反馈。
我特别看重"人工介入率"这个指标。它直接反映了Agent的自主程度。如果人工介入率很高,说明Agent要么能力不足,要么边界太保守,需要针对性优化。
6. 一些实操心得和踩坑记录
6.1 不要过早追求"全自主"
我见过不少团队,一上来就想做"完全自主的Agent",结果做出来的东西没人敢用。我的建议是从"人机协作"开始,逐步提升自主度。
具体路径是:先做"Agent建议+人执行",再做"Agent执行+人确认",最后做"Agent自主执行+异常上报"。每一步都要积累足够的信任和数据,再进入下一步。
6.2 日志要记全,但不要全塞进上下文
日志和上下文是两回事。日志要记全,方便排查问题;上下文要精简,只放LLM需要的信息。我见过把完整日志塞进上下文的做法,结果LLM被无关信息干扰,表现大幅下降。
正确的做法是:日志存到独立的存储系统,上下文只放经过筛选的关键信息。需要排查问题时,从日志系统查,而不是从上下文里翻。
6.3 Prompt版本管理很重要
Agent的Prompt会频繁调整,每次调整都可能影响效果。如果没有版本管理,出了问题都不知道是哪个版本导致的。
我的做法是:Prompt和代码一样,纳入版本控制。每次修改都记录修改原因、修改内容、效果对比。这样出问题时可以快速回滚到上一个稳定版本。
6.4 测试用例要覆盖"边界情况"
Agent的测试不能只测正常流程,更要测边界情况。我通常会准备这几类测试用例:
- 正常流程:用户提供完整信息,Agent顺利完成。
- 信息缺失:用户没提供必要参数,Agent应该主动询问。
- 参数错误:用户提供了格式错误的参数,Agent应该识别并纠正。
- Tool失败:模拟Tool调用失败,Agent应该优雅处理。
- 恶意输入:用户试图诱导Agent做越权操作,Agent应该拒绝。
这些测试用例要定期跑,确保每次修改后Agent的行为都符合预期。
6.5 性能优化的几个关键点
Agent的性能瓶颈通常在三个地方:LLM调用、Tool调用、上下文组装。
- LLM调用:能用小模型的地方就用小模型。比如意图识别用7B模型就够了,不需要上70B。
- Tool调用:能并行就并行。比如同时查询订单和用户信息,不要串行。
- 上下文组装:能缓存就缓存。系统指令、Tool列表这些不变的部分,缓存起来避免重复组装。
我实测下来,做好这三点,Agent的响应时间能降低一半以上。
7. 关于未来演进的一点个人判断
Agent从工具到伙伴的转变,本质上是从"执行指令"到"理解意图"的转变。工具时代,用户要告诉Agent每一步怎么做;伙伴时代,用户只需要说想要什么,Agent自己规划路径。
这个转变对工程的要求更高了。因为工具时代,出错最多是"没执行";伙伴时代,出错可能是"执行错了"。所以Harness层的状态管理、错误恢复、安全边界会越来越重要。
我个人的判断是:未来Agent的竞争力,不在模型,而在Harness工程。模型大家都能用,但怎么把模型、Tool、Skill、记忆、安全这些组件稳定地组装起来,是每个团队要自己解决的问题。这也是为什么我花这么多篇幅讲Harness,而不是讲模型选型。
最后分享一个我一直在用的原则:Agent的每一个决策,都要能解释清楚为什么。如果Agent做了一个决定,你无法从日志和上下文中还原出它的推理过程,那这个Agent就是不可信的。可解释性不是锦上添花,是Agent能被真正用起来的前提。