1. 为什么现在学 Agent 开发,得先搞懂 Harness 这层
1.1 从“会调 API”到“能跑住一个 Agent”之间差了什么
很多人第一次接触 Agent 开发,脑子里想的都是“我调一下大模型接口,让它自己规划、自己调工具,不就完事了”。真上手写两天就会发现,事情根本不是这样。模型能规划,但它规划完的每一步谁来执行?工具调用失败了谁来重试?上下文超了谁来裁剪?跑到一半进程挂了,状态存哪?这些问题,模型本身一个都解决不了。
这中间缺的那一层,就是Harness。你可以把它理解成 Agent 的“骨架 + 神经系统”:模型是大脑,负责想;Harness 是身体,负责把“想”变成“做”,并且保证做的过程可控、可观测、可恢复。没有 Harness,你写出来的东西只能叫“一次性的模型调用脚本”,跑三次崩两次,根本称不上 Agent 应用。
我见过太多人卡在这个坎上:LangChain 的文档翻了一遍,DeepAgents 的示例也跑了,但一到自己写业务逻辑就懵——不知道该在哪一层加东西。原因就是没把 Harness 这层单独拎出来理解。它不是一个库的名字,而是一个职责边界:循环控制、工具调度、状态管理、错误处理、上下文治理,这五件事归它管。
1.2 两天入门的时间账是怎么算出来的
“两天入门”这个说法听起来像营销,但我实际带人走一遍,确实能压到两天,前提是路径不能错。时间大致这么分:
| 阶段 | 内容 | 建议耗时 |
|---|---|---|
| 第一天上午 | 搞清 Agent 与 Harness 的职责划分,跑通最小循环 | 3 小时 |
| 第一天下午 | 接入工具调用,理解工具描述与参数校验 | 3 小时 |
| 第二天上午 | 状态管理与上下文裁剪,处理长对话 | 3 小时 |
| 第二天下午 | 错误重试、并发控制、可观测性埋点 | 3 小时 |
关键在于,不要一上来就啃框架源码。LangChain、DeepAgents 这些框架封装得很好,但封装越好,你越看不清底下发生了什么。正确的顺序是:先用几十行原生代码手写一个最小 Harness,把循环、工具、状态这三件事跑通,再去看框架是怎么帮你省事的。这样你看框架代码时,脑子里有对照物,理解速度完全不一样。
提示:如果你之前只写过 prompt 调用,没写过带循环和状态管理的程序,建议先把“状态机”这个概念补一下。Agent 的本质就是一个带工具调用的状态机,这个认知能帮你省掉大量困惑。
1.3 这本小册子适合谁,不适合谁
适合的人很明确:写过 Python 或 TypeScript,调过大模型 API,想从“玩 prompt”进阶到“做产品级 Agent 应用”的开发者。你不需要懂分布式,不需要懂 K8s,但得能看懂异步代码和基本的错误处理。
不太适合两类人:一类是纯产品经理,想了解 Agent 能做什么但不打算写代码,那看概念科普就够了,没必要啃 Harness 实现;另一类是已经在大厂做 Agent 平台、天天跟调度器打交道的老手,这里讲的东西对你来说太基础。
我自己的判断标准是:如果你能说清楚“工具调用返回结果后,下一步该谁决定”,那你就该学 Harness 了。说不清楚,说明你还在“调 API”阶段,先把这层补上。
2. 核心概念拆解:Agent、Harness、框架到底谁管谁
2.1 Agent 是什么,别被各种定义绕晕
网上关于 Agent 的定义能列出一百种,但落到代码里,Agent 就是一个能根据当前状态决定下一步动作的实体。这个“动作”可以是调用工具、可以是生成回复、也可以是结束任务。它和普通程序最大的区别是:下一步做什么,不是写死的,是模型根据上下文推理出来的。
这里有个特别容易混淆的点:很多人把“用了大模型”等同于“Agent”。不是的。你写个函数,把用户输入拼进 prompt,调一次模型返回结果,这叫模型调用,不叫 Agent。只有当模型能多轮自主决策,并且决策结果会影响后续执行路径时,才叫 Agent。
举个具体例子。用户说“帮我查一下北京明天天气,如果下雨就提醒我带伞”。普通模型调用只能返回一段文字。Agent 会这么做:第一步决定调用天气查询工具,拿到结果;第二步判断结果里有没有雨;第三步根据判断决定要不要调用提醒工具。这三步是模型自己串起来的,不是你写 if-else 串的。这个“自己串起来”的能力,才是 Agent 的核心。
2.2 Harness 的五个核心职责,一个都不能少
Harness 这个词在工程语境里本来指“测试夹具”或“线束”,放到 Agent 里,它指的是承载 Agent 运行的那套基础设施。我把它拆成五个职责,你可以对照自己写的代码,看缺了哪个:
- 循环控制:决定 Agent 什么时候继续、什么时候停。最常见的是“最大轮次限制”,防止模型陷入死循环一直调工具。
- 工具调度:把模型输出的“我要调用某工具”翻译成真实的函数调用,再把结果塞回上下文。这里涉及工具描述解析、参数校验、结果格式化。
- 状态管理:保存对话历史、工具调用记录、中间变量。没有状态管理,Agent 就是失忆的,第二轮就不知道第一轮干了啥。
- 错误处理:工具调用失败、模型返回格式错误、超时,这些都得有兜底策略。重试、降级、报错,选哪个取决于场景。
- 上下文治理:对话一长,token 就爆。裁剪、摘要、检索,都是在这一层做的。
这五件事,框架会帮你做一部分,但你必须在脑子里有清晰的边界。因为框架出问题的时候,你得知道去哪一层找原因。我排查过最久的一个 bug,就是工具调用结果没被正确塞回上下文,导致模型反复调同一个工具。问题出在“工具调度”和“状态管理”的衔接处,框架文档里根本没提这个细节。
2.3 LangChain、DeepAgents 这些框架,到底帮你省了什么
LangChain 的定位是“Agent 开发的全家桶”,它把模型调用、工具定义、记忆管理、链式编排都封装了。DeepAgents 更聚焦,主打深度任务分解和多步推理。这两个我都用过,说点实在的。
LangChain 的优势是生态全,你想接什么模型、什么向量库、什么工具,基本都有现成封装。但它的抽象层数多,一个简单的工具调用能穿过四五层封装,出问题的时候堆栈能打印两屏。DeepAgents 相对轻,它的设计思路是“让 Agent 自己拆解复杂任务”,适合那种需要多步规划的场景,比如“帮我调研一个技术方案并写出对比报告”。
但不管用哪个框架,Harness 的那五个职责一个都不会消失,只是从你写变成了框架写。所以我的建议是:先用原生代码把这五个职责各实现一个最简版本,哪怕只有二十行,然后再上框架。这样你用框架的时候,知道它在哪个位置替你做了什么事,出问题也能定位。
| 框架 | 主要封装 | 适合场景 | 注意点 |
|---|---|---|---|
| LangChain | 模型、工具、记忆、链 | 快速搭原型,生态集成多 | 抽象层多,调试成本高 |
| DeepAgents | 任务分解、多步推理 | 复杂任务规划 | 相对新,文档还在完善 |
| 原生手写 | 无 | 学习原理,定制化需求 | 什么都得自己写 |
2.4 一个最小 Harness 的伪代码长什么样
说了这么多概念,不如直接看结构。下面是我手写最小 Harness 的骨架,语言用 Python 示意,重点是结构不是语法:
class MinimalHarness: def __init__(self, model, tools, max_turns=10): self.model = model self.tools = {t.name: t for t in tools} self.max_turns = max_turns self.history = [] # 状态管理 def run(self, user_input): self.history.append({"role": "user", "content": user_input}) for turn in range(self.max_turns): # 循环控制 response = self.model.chat(self.history) if response.is_final: return response.content # 工具调度 for call in response.tool_calls: result = self._execute_tool(call) self.history.append({"role": "tool", "content": result}) return "达到最大轮次,任务未完成" def _execute_tool(self, call): tool = self.tools.get(call.name) if not tool: return f"工具 {call.name} 不存在" try: return tool.run(**call.args) # 错误处理 except Exception as e: return f"工具执行失败: {e}"这四十行代码,把循环、工具、状态、错误处理都覆盖了。上下文治理没写,因为最小版本可以先不管。你把这四十行跑通,再去看 LangChain 的 AgentExecutor 源码,会发现它做的事情本质上就是这四十行的工程化版本,加了一堆边界处理和可观测性。
3. 实操路径:两天怎么把 Harness 跑起来
3.1 第一天上午:跑通最小循环,别碰框架
第一天上午的目标只有一个:让模型能连续对话两轮以上,并且第二轮能记住第一轮的内容。听起来简单,但这是从“调 API”到“Agent”的关键一步。
具体操作:选一个你熟悉的模型接口,DeepSeek 或者别的都行,写一个 while 循环,把每轮的用户输入和模型输出都追加到一个列表里,下一轮把整个列表传回去。跑通之后,你会看到模型在第二轮能引用第一轮的内容,这就说明状态管理的最小版本成立了。
这一步的坑在于:很多模型的接口对消息格式有要求,比如 role 必须是 user/assistant 交替,或者 tool 消息必须跟在 assistant 消息后面。我第一次跑的时候,因为消息顺序不对,模型直接返回空。排查了半天才发现是格式问题。所以建议你第一步就把消息格式校验加上,省得后面踩坑。
注意:这个阶段不要加工具调用,就纯对话。工具调用会引入新的消息类型,把问题复杂化。先把纯对话的多轮状态跑稳,再加工具。
3.2 第一天下午:接入第一个工具,理解参数校验
工具调用是 Agent 的灵魂,但也是最容易出问题的地方。下午的任务是:定义一个最简单的工具,比如“查询当前时间”或者“计算两个数之和”,让模型能调用它。
工具定义一般包含三部分:名称、描述、参数 schema。名称和描述是给模型看的,模型根据描述决定什么时候调这个工具。参数 schema 是给你自己用的,用来校验模型传过来的参数对不对。
这里有个特别关键的经验:工具描述要写得像给新人看的文档。你写“查询时间”,模型可能不知道查哪个时区。你写“查询指定时区的当前时间,参数 timezone 为 IANA 时区名,如 Asia/Shanghai”,模型调用准确率会高很多。我做过对比,描述写详细之后,工具调用的参数错误率能降一半以上。
参数校验这块,一定要做。模型有时候会传字符串 “5” 而不是数字 5,有时候会漏参数。你不校验,工具内部就报错,错误信息还很难看。校验之后,你可以返回一个友好的错误提示给模型,让它重新调。这就是 Harness 错误处理职责的一部分。
3.3 第二天上午:状态管理与上下文裁剪
到了第二天,对话轮次一多,token 就会爆。这时候必须做上下文治理。最粗暴的做法是“只保留最近 N 轮”,但这样会丢失早期的重要信息。好一点的做法是“摘要 + 保留最近 N 轮”,把早期对话压缩成一段摘要。
摘要怎么做?我的做法是:当历史消息超过阈值时,把最早的一半消息拿出来,让模型生成一段摘要,然后用摘要替换掉那一半消息。这样既保留了关键信息,又控制了 token 量。实测下来,一个 20 轮的对话,压缩后 token 能降到原来的三分之一。
状态管理还有一个容易被忽略的点:工具调用记录也要存。模型调了什么工具、传了什么参数、拿到什么结果,这些都得在历史里。不然模型第二轮会重复调同一个工具。我见过一个 bug,就是因为工具结果没存进历史,模型连续调了五次同一个查询,白白烧了 token。
3.4 第二天下午:错误重试与并发控制
最后半天处理两个工程问题:错误重试和并发。
错误重试的策略取决于错误类型。工具超时,可以重试;参数错误,重试没用,得让模型重新生成参数;模型返回格式错误,可以重试一次,还错就降级。我的做法是给每个工具配一个重试策略,最多重试两次,两次都失败就把错误信息返回给模型,让它决定下一步。
并发控制是很多人忽略的。如果你的 Agent 要同时服务多个用户,每个用户一个会话,那状态管理就得按会话隔离。最简单的做法是用一个字典,key 是会话 ID,value 是历史列表。但要注意,如果工具调用是异步的,得加锁或者用队列,不然会出现状态覆盖。我早期写的一个版本就是因为没做隔离,两个用户的对话串到了一起,排查了好久。
| 问题类型 | 重试策略 | 说明 |
|---|---|---|
| 工具超时 | 重试 2 次 | 可能是网络抖动 |
| 参数错误 | 不重试,返回模型 | 重试也是错的 |
| 格式错误 | 重试 1 次 | 再错就降级 |
| 模型限流 | 退避重试 | 指数退避 |
4. 踩坑实录:那些文档里不会写的细节
4.1 工具描述写不好,模型调用准确率直接腰斩
这是我踩过最大的坑。一开始我觉得工具描述随便写写就行,反正模型聪明。结果实测下来,描述模糊的工具,调用准确率只有六成左右。后来我把每个工具的描述都改成“功能 + 参数说明 + 使用场景 + 返回格式”四段式,准确率直接上到九成以上。
举个例子。模糊描述:“查询天气”。详细描述:“查询指定城市指定日期的天气情况。参数 city 为城市名,date 为日期,格式 YYYY-MM-DD。返回温度、天气状况、降水概率。适用于用户询问天气相关问题时调用。”后者明显更好。
还有一个细节:工具名称不要用缩写。你写qry_wthr,模型可能理解不了。写query_weather,一目了然。这个细节小,但影响大。
4.2 上下文裁剪裁过头,Agent 直接失忆
上下文裁剪的阈值设置很讲究。设太小,频繁裁剪,Agent 失忆;设太大,token 爆了,接口报错。我的经验值是:保留最近 6 到 8 轮完整对话,更早的做摘要。这个值不是固定的,取决于你的模型上下文窗口和每轮对话的平均长度。
裁剪的时候有个坑:不要把工具调用和它的结果裁开。如果你把 assistant 的 tool_call 消息裁掉了,但保留了 tool 的结果消息,模型会懵,因为它不知道这个结果对应哪个调用。所以裁剪的粒度应该是“一轮完整的交互”,包括用户输入、模型输出、工具调用、工具结果。
4.3 并发场景下状态串了,排查了两小时
前面提过并发隔离的问题,这里展开说。我写过一个客服 Agent,同时服务多个用户。测试的时候单用户没问题,一上并发就出乱子,A 用户的对话里出现了 B 用户的订单信息。排查发现是历史列表用了全局变量,没按会话隔离。
修复方法很简单,用一个字典按 session_id 存历史。但这里有个隐藏问题:如果工具调用是异步的,字典的读写要加锁。Python 的 GIL 在 IO 操作时会释放,所以异步场景下不加锁还是可能出问题。我后来改成了每个会话一个独立的 Harness 实例,彻底隔离,问题就没了。
提示:如果你的 Agent 要上生产,并发隔离一定要在开发阶段就做好。后期改这个,涉及状态管理的所有代码,成本很高。
4.4 模型返回格式不稳定,加一层解析兜底
不同模型返回工具调用的格式不一样。有的返回 JSON,有的返回特定标记。更麻烦的是,同一个模型,有时候返回的 JSON 字段名还不一致。我遇到过模型把tool_name写成name,把arguments写成args,解析直接失败。
解决办法是加一层格式适配器,把不同格式统一成内部标准格式。适配器里做字段名映射和类型转换。这样上层 Harness 只认标准格式,模型换了也不用改上层代码。这个适配器我写了大概五十行,但省掉了后面无数次改代码的麻烦。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型反复调同一工具 | 工具结果没存进历史 | 检查状态管理 |
| 工具调用参数错误 | 工具描述不清晰 | 完善描述和 schema |
| 第二轮对话失忆 | 历史没传回去 | 检查消息拼接 |
| token 超限报错 | 上下文没裁剪 | 加裁剪逻辑 |
| 并发时状态串了 | 历史没按会话隔离 | 加 session 隔离 |
| 模型返回解析失败 | 格式不统一 | 加格式适配器 |
5. 从能跑到能用:再往前一步的扩展方向
5.1 可观测性:给 Harness 装上眼睛
能跑起来之后,下一步是让它“可观测”。具体说,就是记录每一轮的输入输出、工具调用耗时、token 消耗、错误率。这些数据不记录,出了问题你只能靠猜。
我的做法是在 Harness 的关键节点埋点:循环开始、模型调用前后、工具调用前后、循环结束。每个点记录时间戳和关键数据。这些日志不用很复杂,打印成结构化 JSON 就行,后面接个日志系统就能看趋势。
有了这些数据,你能回答很多问题:哪个工具最慢、哪类问题 token 消耗最高、错误集中在哪个环节。这些信息对优化 Agent 至关重要。
5.2 工具生态:从单工具到工具集
单个工具跑通后,你会想加更多工具。这时候要考虑工具的组织方式。我的建议是按领域分组,比如“数据查询类”“文件操作类”“通知类”,每组工具有统一的命名前缀。这样模型在选择工具时,描述里的领域信息能帮它更快定位。
工具多了之后,还有一个问题:工具描述的总长度会占用大量 token。如果工具有几十个,光描述就上千 token。解决办法是动态加载:根据当前对话的意图,只把相关工具的描述传给模型。这个需要一点意图识别,但能大幅节省 token。
5.3 评估:怎么知道你的 Agent 变好了
Agent 开发最麻烦的是评估。模型调用可以看准确率,但 Agent 是多步决策,怎么评?我的做法是建一个小型测试集,每个测试用例包含用户输入和期望的工具调用序列。跑一遍,看实际调用序列和期望的匹配度。
这个测试集不用很大,二十个用例就能覆盖大部分场景。关键是每次改完 Harness 都跑一遍,确保没把之前的功能改坏。我吃过这个亏,改上下文裁剪逻辑的时候,把工具调用记录裁掉了,导致一批用例失败,但当时没测,上线才发现。
评估指标可以看这几个:任务完成率、平均轮次、工具调用准确率、平均 token 消耗。这几个指标一起看,能比较全面地反映 Agent 的健康状况。
5.4 我个人的一点体会
学 Agent 开发,最大的障碍不是技术难度,是概念混乱。市面上各种框架、各种术语,把人绕得晕头转向。我的建议是,先别管那些名词,就抓住 Harness 这五个职责:循环、工具、状态、错误、上下文。把这五个东西用最朴素的方式实现一遍,你脑子里就有了清晰的骨架。之后再学任何框架,都是往这个骨架上挂东西,速度完全不一样。
两天入门,不是说两天能成为专家,是说两天能建立起正确的认知框架。有了这个框架,后面深入就是时间问题。没有这个框架,看再多文档也是零散的,串不起来。
最后分享一个小技巧:把你写的 Harness 打印出来贴在显示器旁边。每次遇到问题,对照那五个职责,看是哪个环节出的问题。这个方法帮我省了大量排查时间。工具调用不对,看工具调度;对话失忆,看状态管理;token 爆了,看上下文治理。问题定位快了,开发效率自然就上来了。