1. 从"Agent 到底在跑什么"说起
很多人第一次接触 AI Agent,脑子里浮现的画面是:一个模型坐在那里,你给它一句话,它自己思考、自己调工具、自己写代码、自己交付结果。但真上手搭过一个能跑起来的 Agent 之后,你会发现一个很反直觉的事实——模型本身其实只占整个系统里很小的一块。真正让 Agent "动起来"的,是外面那一圈被反复调用、反复拼接、反复兜底的工程骨架。
这个骨架,业内现在有个越来越流行的叫法:Harness。
直译过来是"马具""挽具",意思很贴切——模型是那匹有劲但方向感不稳定的马,Harness 就是套在它身上、把它的力气导向正确方向的那套缰绳、鞍具和车架。没有 Harness,模型再强也只是一个"会聊天的函数";有了 Harness,它才能变成一个能读文件、能跑命令、能改代码、能自我纠错的"干活实体"。
我最初也以为 Harness 是个很玄的东西,直到自己从零拆了一遍,才发现它其实就由7 个子系统拼起来。这 7 块拆清楚之后,你再去看任何一款 Agent 产品,基本都能对号入座——它强在哪、弱在哪、为什么有时候"聪明"有时候"智障",一眼就能看穿。
这篇内容适合三类人:一是想从 0 到 1 搭一个自己的 Agent、但被各种框架绕晕的开发者;二是想理解 Agent 产品底层差异、做技术选型的产品和架构同学;三是面试里被问到"Agent Loop 怎么设计""工具调用怎么兜底"这类问题、想有个清晰框架的人。我会按"这 7 个子系统分别解决什么问题、为什么必须存在、实际怎么落地、容易踩什么坑"的顺序讲,尽量说人话,能抄的配置和思路我直接给出来。
先给一张全局图(文字版,方便你记):
| 子系统 | 一句话职责 | 缺了会怎样 |
|---|---|---|
| Agent Loop | 驱动"思考-行动-观察"循环 | Agent 只回答一次就停 |
| LLM Integration | 把模型接进来并管理上下文 | 没有大脑 |
| Tool / Action Layer | 给模型可调用的手和脚 | 只能空谈不能干活 |
| Memory & State | 记住历史与当前任务状态 | 每轮都失忆 |
| Context Management | 决定什么进上下文、什么被裁掉 | 上下文爆炸或关键信息丢失 |
| Guardrails & Safety | 拦截危险动作、限制越权 | 一个 rm -rf 教你做人 |
| Observability & Eval | 记录、回放、评测每一步 | 出问题只能靠猜 |
下面逐个拆。
2. Agent Loop:整个 Harness 的心跳
2.1 为什么必须有 Loop,而不是"一问一答"
普通聊天是"输入→输出"一次就结束。但 Agent 干活是多步的:读需求→查文件→改代码→跑测试→看报错→再改。这个"再改"就意味着必须有一个循环,让模型能基于上一轮的观察结果决定下一步。
这个循环最经典的形态就是 ReAct(Reason + Act):模型先输出一段"思考",再输出一个"动作"(比如调用某个工具),Harness 执行这个动作拿到"观察结果",把结果塞回上下文,再让模型继续思考。如此往复,直到模型输出一个"结束"信号,或者达到最大步数。
我见过太多新手写的"Agent",本质就是一个 while 循环里塞了一次模型调用,没有终止条件、没有步数上限、没有异常退出。结果要么死循环烧钱,要么跑两步就断。Loop 的设计质量,直接决定 Agent 是"能用"还是"能打"。
2.2 一个能落地的 Loop 骨架长什么样
用伪代码表达,一个稳健的 Loop 大概是这样:
def run_agent(task, max_steps=25): state = init_state(task) for step in range(max_steps): # 1. 组装当前上下文 context = build_context(state) # 2. 调用模型 response = llm.invoke(context, tools=available_tools) # 3. 解析模型输出 action = parse_action(response) # 4. 终止判断 if action.type == "finish": return action.payload # 5. 执行动作(带超时和异常捕获) try: observation = execute(action, timeout=30) except Exception as e: observation = f"工具执行失败: {e}" # 6. 把观察结果写回状态 state.append(step, action, observation) return "达到最大步数,任务未完成"这里面有几个容易被忽略但极其关键的点:
- max_steps 必须有。没有上限的循环就是烧钱机器。我一般设 20~30 步,复杂任务可以到 50,但一定要有。
- 工具执行必须带超时。某个工具卡死,整个 Agent 就挂住。30 秒是个比较通用的默认值,跑测试这类可以放宽到几分钟。
- 异常要转成观察结果,而不是直接抛出。模型看到"工具执行失败: xxx"反而可能自己换个思路,直接抛异常就整个崩了。
- 终止信号要显式。让模型通过一个专门的
finish动作结束,比让它"自然停止输出"可靠得多。
2.3 循环里最容易翻车的三个地方
第一个是步数浪费。模型经常在"我已经知道答案了"和"再确认一下"之间反复横跳,白白多跑好几轮。解决办法是在系统提示里明确写"如果信息已足够,请立即调用 finish,不要重复验证"。
第二个是动作解析失败。模型输出的格式偶尔会跑偏,比如该输出 JSON 却输出了一段自然语言。Harness 必须有一个容错解析器:先尝试严格解析,失败就尝试正则提取,再失败就把"格式错误"作为观察结果喂回去让它重试。我一般给 2 次重试机会,还不行就终止。
第三个是观察结果过长。比如跑一个测试,输出几千行日志,全塞回上下文直接爆掉。这里就要靠后面讲的 Context Management 来裁剪,Loop 层只负责"把结果交出去",不负责"怎么塞"。
提示:Loop 的每一步都应该打日志(step 编号、动作、耗时、token 消耗)。没有这些日志,你根本不知道 Agent 是在"思考"还是在"空转"。
3. LLM Integration:把"大脑"接进来,远不止调个 API
3.1 模型接入要处理的四件事
很多人以为 LLM Integration 就是client.chat.completions.create(...)一行。真做起来,你要处理的是四件事:模型选择、请求封装、响应解析、失败重试。
模型选择上,现在主流是"强模型做规划、快模型做执行"的混合策略。规划步骤(决定下一步干什么)用能力强的模型,执行步骤(比如格式化输出、简单分类)用便宜快的模型。这样能在成本和效果之间取得不错的平衡。我实测下来,把简单步骤下沉到小模型,整体成本能降 40% 以上,效果几乎无损。
请求封装要统一抽象。因为不同模型的 API 参数、工具调用格式、流式返回方式都不一样。Harness 里应该有一个LLMProvider接口,把差异屏蔽掉,上层 Loop 只认统一接口。这样换模型时只改一处。
3.2 工具调用(Function Calling)的坑
工具调用是 LLM Integration 里最容易出问题的地方。不同模型对工具调用的支持程度差异很大:有的原生支持结构化 function call,有的只能靠提示词"诱导"模型输出特定格式。
我的经验是:不要完全依赖模型的原生工具调用能力。稳妥的做法是双轨制——如果模型支持原生 function call,优先用;如果不支持或支持不稳定,就用提示词约定格式 + 解析器兜底。这样换模型时不会因为工具调用格式不兼容而整个瘫痪。
还有一个细节:工具描述的质量直接决定模型会不会用对工具。工具名要语义清晰(read_file比rf好一万倍),描述要说清楚"什么时候用、参数是什么、返回什么"。我见过因为工具描述写得含糊,模型反复调错工具、浪费十几步的案例。
3.3 上下文窗口与 token 预算
每个模型都有上下文窗口上限。Harness 必须实时估算当前上下文的 token 数,并在接近上限时触发裁剪。这里有个实用技巧:预留 20% 的窗口给模型输出。比如 128k 窗口的模型,输入最多用到 100k 左右,剩下的留给它生成。
token 估算不需要特别精确,按"字符数 / 3"粗估中文、"字符数 / 4"粗估英文就够用了。精确计算反而拖慢速度,得不偿失。
4. Tool / Action Layer:Agent 的手和脚
4.1 工具不是越多越好
新手最容易犯的错是"工具堆砌"——恨不得把能想到的工具全塞进去。结果模型面对几十个工具,选择困难,调用准确率暴跌。工具数量控制在 10~15 个以内是比较舒服的区间,超过就要考虑分组或按需加载。
工具设计有个原则:一个工具做一件事,粒度适中。太粗(比如一个do_everything)模型不知道怎么用;太细(比如把"读文件"拆成"打开""读取""关闭"三个工具)模型要调三次,浪费步数。
4.2 工具执行的安全边界
工具是 Agent 真正"动手"的地方,也是风险最集中的地方。必须设几道防线:
- 白名单:只允许访问指定目录、指定命令。文件操作限制在工作区内,命令执行限制在允许列表内。
- 超时:每个工具调用都有超时,防止卡死。
- 资源限制:限制单次输出大小(比如最多返回 10000 字符),防止上下文被撑爆。
- 幂等性考虑:写文件、发请求这类有副作用的操作,要考虑重复执行会不会出问题。
我踩过最狠的一个坑:早期没做目录白名单,模型在调试时"顺手"把工作区外的文件改了。从那以后,所有文件类工具都强制做路径规范化 + 前缀校验,确保只能在工作区内操作。
4.3 工具返回结果的处理
工具返回的东西不能原样塞回上下文。要做三件事:截断(超长输出只保留头尾 + 中间省略)、结构化(能转成简洁 JSON 就别返回大段文本)、错误归一化(把各种异常统一成模型能理解的描述)。
举个实际例子:跑测试失败时,原始输出可能有几百行。我会提取出"失败的测试名 + 错误类型 + 关键堆栈前 5 行",其余丢掉。模型看到这个精简版,反而更容易定位问题。
5. Memory & State:让 Agent 不在原地打转
5.1 短期状态 vs 长期记忆
Memory 分两层。短期状态是当前任务的"工作台"——已经做了哪些步骤、拿到了什么中间结果、当前进度到哪。长期记忆是跨任务的知识——用户偏好、项目约定、历史经验。
大部分 Agent 只需要做好短期状态就够了。长期记忆是进阶需求,做不好反而引入噪声。我的建议是:先把短期状态做扎实,长期记忆等有明确场景再加。
5.2 状态该怎么存
状态不是简单地把所有消息堆成一个列表。那样做,上下文会迅速膨胀,而且模型很难从中提取"当前该关注什么"。
更好的做法是结构化状态:
{ "task": "修复登录接口的空指针异常", "steps": [ {"step": 1, "action": "read_file", "target": "auth/login.py", "result": "已读取"}, {"step": 2, "action": "search", "query": "NullPointer", "result": "定位到第 42 行"} ], "findings": ["第 42 行未判空"], "current_focus": "修改第 42 行", "artifacts": {"modified_files": ["auth/login.py"]} }这样组装上下文时,可以只把findings、current_focus和最近几步塞进去,历史步骤只保留摘要。既省 token,又让模型聚焦。
5.3 状态压缩的时机
当步骤数超过一定阈值(比如 15 步),就该触发一次状态压缩:把前面的步骤总结成一段"到目前为止发生了什么",替换掉原始步骤记录。这个总结可以让模型自己生成,也可以规则化提取。
我一般用规则 + 模型混合:关键动作(文件修改、命令执行)保留原始记录,纯查询类动作合并成摘要。这样既保住了关键信息,又控制了体积。
6. Context Management:决定 Agent 聪明还是智障的关键
6.1 上下文里到底该放什么
这是整个 Harness 里最考验功力的一块。上下文放得好,模型表现像天才;放得乱,再强的模型也变智障。
一个健康的上下文通常包含:系统提示(角色 + 规则 + 工具说明)、任务描述、当前状态摘要、最近几步的详细记录、当前可用的工具列表。注意"最近几步详细、更早的摘要"这个分层结构,是控制体积的核心技巧。
6.2 裁剪策略:不是简单截断
上下文超限时的裁剪,绝不能简单地从头部砍。那样会把系统提示和任务描述砍掉,模型直接失忆。
正确的优先级是:系统提示和任务描述永远保留 > 当前状态和最近步骤保留 > 更早的步骤压缩成摘要 > 工具返回的大块内容优先裁剪。
我常用的一个策略叫"滑动窗口 + 锚点":保留最前面的系统提示(锚点)和最后 N 步(滑动窗口),中间部分压缩。这样模型既有稳定的行为准则,又有新鲜的上下文。
6.3 一个实测有效的上下文模板
[系统提示] 你是 xxx Agent,遵循以下规则:... [任务] {task} [当前状态] 进度:{current_focus} 已知发现:{findings} [最近操作] {最近 5 步的详细记录} [历史摘要] {更早步骤的压缩摘要} [可用工具] {tools}这个模板我用了很久,稳定性不错。关键是把"当前该关注什么"(current_focus)显式写出来,模型不容易跑偏。
7. Guardrails & Safety:别等出事才想起来
7.1 输入、执行、输出三道关
Guardrails 要覆盖三个环节。输入关:过滤明显恶意的指令。执行关:拦截危险的工具调用(删文件、改系统配置、访问敏感路径)。输出关:检查最终结果里有没有不该出现的内容。
执行关是最重要的。我的做法是给每个工具定义"风险等级",高风险工具(写、删、执行命令)在执行前做二次校验——要么规则校验,要么让模型自己确认一遍"你确定要执行这个操作吗"。
7.2 权限最小化原则
Agent 应该只拥有完成任务所需的最小权限。文件操作限制在工作区,命令执行限制在白名单,网络访问限制在必要域名。这不是不信任模型,而是工程上的基本防御——模型会犯错,系统要能兜住。
7.3 人工确认的插入点
对于不可逆的操作(删除、发布、支付),应该插入人工确认环节。Harness 在这一步暂停,把"即将执行的操作"展示给用户,等确认后再继续。这个机制在自动化流程里尤其重要。
8. Observability & Eval:没有它,你就是在盲调
8.1 每一步都要可回放
Agent 出问题时,最痛苦的是"不知道它为什么这么做"。所以每一步的输入上下文、模型输出、工具调用、返回结果、耗时、token 消耗,都要完整记录。有了这些,你才能回放整个决策链路。
我习惯把每次运行的 trace 存成结构化日志,出问题时直接按 step 回放。这个习惯帮我定位过无数次"模型为什么突然跑偏"的问题——十有八九是某一步的观察结果里混进了干扰信息。
8.2 评测不能只看最终结果
Agent 的评测要看过程指标:步数是否合理、工具调用是否准确、有没有无效循环、token 消耗是否超标。只看最终对不对,会漏掉大量问题。
我一般维护一个小型评测集(20~50 个典型任务),每次改动 Harness 后跑一遍,对比成功率、平均步数、平均成本三个指标。这三个指标一起看,基本能判断改动是优化还是劣化。
8.3 常见问题的排查路径
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 反复调同一个工具 | 观察结果没被正确理解 | 看该步上下文是否完整 |
| 步数暴涨 | 缺少终止引导 | 检查系统提示的 finish 规则 |
| 突然跑偏 | 上下文混入噪声 | 回放定位哪一步引入 |
| 成本飙升 | 上下文膨胀 | 检查裁剪策略是否生效 |
| 工具调错 | 工具描述不清 | 优化工具名和描述 |
9. 把这 7 块拼起来:一个最小可跑 Harness 的组装顺序
讲了这么多,最后说说实际搭建时的顺序。我的建议是:
- 先做 LLM Integration + 最简 Loop:能跑通"模型调用→解析→执行→回填"这个闭环就行,工具先给一两个。
- 加 Tool Layer:把常用工具补齐,做好超时、白名单、结果截断。
- 加 State 和 Context Management:这是从"能跑"到"能稳定跑"的关键一步。
- 加 Guardrails:在工具执行前插入校验。
- 加 Observability:从第一天就该有日志,但完整的回放和评测可以稍后补。
- 最后做 Memory 和 Eval:这是进阶优化。
不要一上来就追求"7 块全齐"。我见过太多人卡在架构设计上,结果一个能跑的 Agent 都没搭出来。先跑起来,再逐步加固,这个顺序比什么都重要。
另外分享一个我自己的体会:Harness 的复杂度应该和任务复杂度匹配。做一个"读文件改代码"的编码 Agent,7 块都要认真做;做一个"查天气"的玩具 Agent,Loop + LLM + 一两个工具就够了。别为了架构而架构,够用就好。
真正拉开 Agent 差距的,从来不是模型多强,而是这套 Harness 做得多细。模型是通用能力,Harness 才是你的护城河。把上面这 7 块一块块抠明白,你搭出来的 Agent 就能从"演示级"跨到"生产级"。