1. 拆开 AI Agent 的引擎盖Harness 到底在管什么很多人第一次听到 Harness 这个词脑子里浮现的是线束、马具、测试框架跟 AI Agent 八竿子打不着。但如果你真正动手搭过一个能跑起来的 Agent就会明白这个名字起得有多贴切——它就是把模型这匹野马套住、让它按既定路线干活的那整套挽具。模型本身只会根据输入吐 token它不知道自己要调哪个工具、什么时候该停、失败了要不要重试、上下文塞满了该丢什么。这些脏活累活全归 Harness 管。我接触过不少团队模型选的是顶配Prompt 写得也漂亮但 Agent 一上生产就各种翻车要么陷入死循环疯狂调同一个接口要么把工具返回的一大坨 JSON 原封不动塞回上下文把窗口撑爆要么该停的时候不停、不该停的时候提前收工。问题几乎都不在模型而在 Harness 这一层没搭明白。所以这篇东西我想干一件事把 Harness 拆成 7 个能单独拎出来看的子系统讲清楚每个子系统负责什么、为什么必须独立存在、以及实际搭的时候有哪些坑。不管你是用现成框架还是从零手写这套拆法都能直接套用。先给个总览Harness 的 7 个子系统分别是Agent Loop主循环、LLM Integration模型接入层、Tool Registry Dispatch工具注册与分发、Context Management上下文管理、Memory记忆系统、State Persistence状态与持久化、Guardrails Observability护栏与可观测性。这七个不是拍脑袋分的而是我按一个 Agent 从收到任务到交付结果中间必然经过哪些环节倒推出来的。少任何一个系统要么跑不起来要么跑起来不可控。提示Harness 和 Agent 不是一回事。Agent 是能自主决策的执行体这个概念Harness 是让这个概念落地的工程骨架。你可以把 Agent 理解成司机Harness 理解成整辆车——发动机、方向盘、刹车、仪表盘缺一不可。下面逐个拆。我会尽量把每个子系统的为什么讲透因为只讲怎么做的话换个场景你就不会迁移了。2. 子系统逐个拆解每个部件为什么必须独立存在2.1 Agent Loop整个系统的心跳Agent Loop 是 Harness 的心脏它决定了 Agent 是跑一轮就死还是能持续干活直到任务完成。最朴素的循环长这样把当前上下文发给模型 → 模型返回要么是最终答案、要么是工具调用请求 → 如果是工具调用执行工具、把结果塞回上下文 → 再发给模型如此往复直到模型给出终止信号或触发最大轮次限制。听起来简单但这里藏着第一个大坑终止条件的设计。我见过太多人只靠模型说完成了来终止结果模型有时候会礼貌性地一直说我还可以继续优化循环停不下来。靠谱的做法是三重保险模型显式返回终止标记、达到最大迭代轮次比如 15 轮、检测到连续 N 轮没有产生新的有效动作。这三个条件任意一个满足就退出退出时还要记录是因为哪个条件退的方便排查。第二个坑是单轮内的动作粒度。有些实现允许模型在一轮里返回多个工具调用并行执行有些则强制一轮一个。我的经验是读操作查询、检索可以并行写操作创建、修改、删除必须串行而且写操作之间要能感知彼此的结果。原因很直白——并行写会产生竞态模型基于旧状态做的第二个决策可能已经失效了。# Agent Loop 的骨架重点看终止条件的多重保险 def agent_loop(task, max_turns15, max_idle_turns3): context build_initial_context(task) idle_count 0 for turn in range(max_turns): response llm_integration.call(context) if response.is_final: return response.content if not response.tool_calls: idle_count 1 if idle_count max_idle_turns: return 任务未完成连续空转退出 continue idle_count 0 results dispatch_tools(response.tool_calls) context context_manager.append(context, response, results) return 达到最大轮次退出这段代码里idle_count那个逻辑是我踩坑之后加的。之前有个项目模型偶尔会返回一个空的工具调用列表但又不给最终答案循环就卡在那儿空转白白烧 token。加上空转计数之后这种情况三轮内必然退出。2.2 LLM Integration别把模型调用当成一个函数很多人把 LLM Integration 简单理解成发个 HTTP 请求拿回复这是最危险的想法。模型接入层要处理的东西远比想象中多多模型路由简单任务用小模型省钱复杂推理用大模型、重试与退避模型服务偶尔抖动是常态、流式与非流式切换前端要打字机效果就得流式但工具调用场景往往需要完整响应、token 计数与成本核算不记账你永远不知道钱花哪了、格式约束强制 JSON 输出、强制工具调用 schema。我特别想强调格式约束这一块。模型返回的工具调用参数如果不做 schema 校验十有八九会在某个边界情况下给你惊喜——比如该传整数的地方传了字符串该传数组的地方传了对象。接入层必须在拿到响应后立刻做校验校验不过就带着错误信息让模型重试而不是把脏数据往下游传。下游工具拿到脏数据报的错往往离真正的问题很远排查起来极其痛苦。还有一个容易被忽略的点超时策略要分层。单次模型调用超时、单轮工具执行超时、整个任务超时这三个是不同层级的。我一般设单次调用 60 秒、单轮工具 30 秒、整任务 10 分钟。整任务超时到了就强制收尾把当前进度和未完成事项返回给用户而不是让用户干等。2.3 Tool Registry Dispatch工具的户口本和调度台工具注册与分发说白了就是两件事有哪些工具可用以及怎么把模型的调用请求路由到正确的工具上。听起来是纯工程活但设计好坏直接决定 Agent 的能力上限。先说注册。每个工具至少要声明名称、描述、参数 schema、执行函数、超时时间、是否幂等、是否需要人工确认。描述这块特别关键——模型是靠描述来决定用不用这个工具的。我见过有人把工具描述写成查询数据模型根本不知道查的是什么数据自然就不会用。好的描述应该像给新员工写操作手册根据用户 ID 查询该用户的订单列表返回最近 30 天的订单参数 user_id 为字符串。再说分发。分发层要做参数校验、权限检查、超时控制、结果截断。结果截断是我要重点提的工具返回的结果可能非常大比如一个查询返回了 500 条记录直接塞回上下文会瞬间吃掉几千 token。分发层应该做智能截断——保留结构、保留前 N 条、附带共 X 条已展示前 N 条的提示让模型知道数据被截断了需要的话可以再查。工具属性作用不填的后果名称模型调用的标识无法被调用描述模型判断是否使用的依据工具被冷落或误用参数 schema校验入参合法性脏数据流入执行层超时时间防止单个工具卡死整个循环任务无限挂起幂等标记决定失败后能否安全重试重复写操作造成脏数据确认标记高危操作需人工介入误删误改无法挽回2.4 Context Management最容易被低估的子系统如果只能保留一个子系统我会选 Context Management。因为 Agent 的智商很大程度上取决于它当前能看到什么。上下文管理要解决三个问题塞什么进去、什么时候丢、怎么压缩。塞什么进去涉及的是信息优先级。系统提示词、当前任务、最近几轮对话、工具调用历史、检索到的外部知识这些信息的价值密度是不一样的。我的做法是分层系统提示词永远保留当前任务和最近 3 轮完整保留更早的对话做摘要工具调用历史只保留调了什么、结果概要原始返回按需回捞。什么时候丢涉及的是窗口预算。假设模型窗口是 128K token你不能等塞满了才想起来丢得留出至少 20% 的余量给模型输出和突发情况。我一般设一个水位线超过 70% 就开始触发压缩流程。怎么压缩这是技术活。最简单的做法是滑动窗口丢掉最老的好一点的是做摘要让模型把早期对话浓缩成几句话再好一点的是结构化记忆把关键事实抽出来存成键值对需要时按 key 检索。我实测下来摘要 结构化记忆的组合效果最好纯滑动窗口在长任务里会丢失关键约束条件导致 Agent 后期行为跑偏。注意压缩上下文时绝对不能丢的是用户明确提出的约束和已经确认的事实。这两类信息丢了Agent 会开始胡说八道或者重复问已经回答过的问题。2.5 Memory短期上下文之外的长期记忆Memory 和 Context Management 经常被混为一谈但它们解决的是不同时间尺度的问题。Context 管的是这一次任务里模型能看到什么Memory 管的是跨任务、跨会话Agent 能记住什么。Memory 一般分三类事实记忆用户偏好、业务规则这类稳定信息、情景记忆过去发生过什么任务、结果如何、语义记忆可检索的知识库。事实记忆适合存结构化数据情景记忆适合存带时间戳的事件流语义记忆适合向量检索。我踩过的一个坑是把什么都往向量库里塞。结果检索出来的东西相关性很差因为很多信息根本不适合用语义相似度来找。比如用户偏好用中文回复这种事实用向量检索纯属浪费直接存键值对、每次拼进系统提示词就行。选对存储介质比选对检索算法更重要。另一个坑是记忆的写入时机。不能每轮都写那样会产生大量冗余也不能任务结束才写那样中途崩溃就全丢了。我的做法是关键节点写任务开始、关键决策、任务结束并且写入前做去重和冲突检测。如果新记忆和旧记忆冲突标记出来让上层决定是覆盖还是并存。2.6 State Persistence让 Agent 能断点续跑Agent 跑长任务时进程可能因为各种原因中断——部署更新、机器重启、任务超时被 kill。如果没有状态持久化一切从头再来前面烧的 token 全白费。State Persistence 要做的就是让 Agent 能存档和读档。状态分两种循环状态当前第几轮、上下文快照、待执行的工具调用和业务状态任务进度、已完成的子目标、中间产物。循环状态用于恢复执行业务状态用于向用户汇报和跨任务复用。实现上最简单的方案是每轮结束把状态序列化存到数据库或对象存储。复杂一点的是事件溯源——把每一步操作记成事件流恢复时重放事件。事件溯源的好处是可审计、可回溯坏处是实现复杂、存储成本高。我的建议是中小规模用快照就够了等真的需要审计和回放再上事件溯源。这里有个细节快照要包含版本号。Harness 本身会迭代今天存的快照明天可能因为代码变更读不出来。带上版本号读档时先检查兼容性不兼容就走降级流程或者提示用户重新发起。2.7 Guardrails Observability刹车和仪表盘最后这个子系统是区分玩具 Agent和生产 Agent的分水岭。Guardrails 是护栏防止 Agent 干出格的事Observability 是可观测性让你知道 Agent 到底在干什么。护栏分输入侧和输出侧。输入侧防的是提示词注入、越权指令输出侧防的是敏感信息泄露、高危操作执行、格式不合规。高危操作删除、支付、对外发送必须走人工确认这个确认不能是模型自己判断要不要确认而应该是工具注册时就标记好、分发层强制执行。可观测性这块最少要记录每轮的输入输出、工具调用及耗时、token 消耗、错误堆栈、终止原因。这些数据不光用于排查还能反哺优化——比如你发现某个工具被调用的频率极高但成功率很低那要么是工具本身有问题要么是描述写得让模型误解了。观测指标用途异常阈值参考单任务轮次判断是否陷入循环超过 10 轮需警惕单任务 token 消耗成本控制超过预算 80% 告警工具调用成功率发现工具有问题低于 90% 需排查平均单轮耗时发现性能瓶颈超过 30 秒需优化终止原因分布判断系统健康度非正常终止占比过高需干预3. 从零搭一个最小可用 Harness实操流程3.1 先定边界什么该自己写什么该用现成的动手之前先想清楚这个问题能省掉大量返工。我的判断标准是Agent Loop、Context Management、Guardrails 这三块建议自己写因为它们和你的业务场景强耦合用现成框架反而要花大量精力去适配。LLM Integration、Tool Registry、Memory、Persistence 这四块可以先用现成方案等业务跑通了再按需替换。为什么 Loop 要自己写因为不同业务的循环逻辑差异很大。客服 Agent 可能需要多轮澄清代码 Agent 需要写-测-改的固定节奏研究 Agent 需要检索-阅读-综合的循环。这些节奏用通用框架表达起来很别扭自己写反而清晰。为什么 Context Management 要自己写因为什么信息重要只有你最清楚。框架给的通用压缩策略往往会把你的关键业务约束给压没了。3.2 搭骨架先让一个最简单的循环跑起来别一上来就追求完整先用最少的代码让模型调用工具、拿到结果、再调用模型这个闭环跑通。这个阶段的目标是验证链路不是追求效果。# 最小可用版本先跑通再说 import json def minimal_harness(task, tools, llm_client, max_turns10): messages [ {role: system, content: 你是一个助手可以使用工具完成任务。}, {role: user, content: task} ] for _ in range(max_turns): resp llm_client.chat(messages, toolstools) msg resp.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: fn TOOL_MAP[call.function.name] args json.loads(call.function.arguments) result fn(**args) messages.append({ role: tool, tool_call_id: call.id, content: str(result)[:2000] # 先粗暴截断 }) return 达到最大轮次这段代码里str(result)[:2000]那个截断是故意的先粗暴截断保证不爆窗口后面再优化成智能截断。很多人的问题是第一步就想做完美结果卡在细节上迟迟跑不起来。3.3 加护栏让循环可控骨架跑通后立刻加三样东西最大轮次、单轮超时、工具白名单。这三样是保命的缺了任何一样都不敢让 Agent 接触真实环境。最大轮次前面说过设 10 到 15 轮比较合适。单轮超时用信号或者线程池实现超时就中断当前工具执行、把超时信息塞回上下文让模型决定下一步。工具白名单是防止模型幻觉出不存在的工具名分发前先查白名单不在名单里直接返回错误让模型重试。3.4 接上下文管理从粗暴截断到分层保留这一步把[:2000]的粗暴截断替换掉。我的做法是给每条消息打标签system、task、recent、history、tool_result。拼上下文时按优先级组装system和task永远全量recent保留最近 3 轮history做摘要tool_result只保留概要加原始数据的引用 ID。摘要怎么做用一个便宜的小模型把早期对话浓缩成用户要求 X已完成 Y当前卡在 Z这样的结构化摘要。实测下来小模型做摘要完全够用成本还低。3.5 接持久化让任务能续跑到这一步把每轮结束的状态存下来。存什么消息列表、当前轮次、任务 ID、创建时间、状态标记。存哪里小规模用 SQLite 就够大规模上 PostgreSQL 或者对象存储。恢复逻辑很简单任务启动时先查有没有未完成的状态有就加载进来接着跑。这里要注意幂等性——如果上一轮的工具调用已经执行了但状态没存成功恢复后可能重复执行。所以写操作工具必须支持幂等键或者状态存储和工具执行要在一个事务里。3.6 接可观测性让问题可追溯最后接日志和指标。每轮记录轮次编号、输入 token 数、输出 token 数、工具调用列表、耗时、结果状态。这些数据打到结构化日志里方便后续查询和告警。我一般还会加一个任务回放功能给定任务 ID能把整个执行过程按时间线展示出来。排查问题时这个功能救命——你能清楚看到模型在第几轮做了什么决策、基于什么信息、结果如何。4. 踩坑实录那些文档里不会写的问题4.1 模型假装调用了工具这个坑很隐蔽。模型有时候会在文本回复里写我将调用查询工具但实际返回的 tool_calls 是空的。如果你只检查文本内容会以为它调用了如果只检查 tool_calls会漏掉它其实想调用但格式没对。我的处理方式是解析响应时同时看文本和 tool_calls如果文本里出现了工具名但 tool_calls 为空就追加一条系统消息提醒模型请使用工具调用格式让它重试。这个重试逻辑加上之后工具调用成功率明显提升。4.2 工具返回的错误信息太技术工具执行失败时如果直接把 Python 的 traceback 塞回上下文模型大概率看不懂会做出莫名其妙的决策。正确的做法是分发层捕获异常后转换成自然语言的错误描述查询用户订单失败原因是用户 ID 格式不正确请检查后重试。更进一步错误信息里应该包含下一步建议。模型看到请检查后重试就知道该换个参数再试看到该用户不存在就知道该换个思路。错误信息的质量直接影响 Agent 的自愈能力。4.3 上下文压缩把关键约束压没了前面提过这里展开说。我遇到过一个案例用户要求只查询 2024 年之后的订单这个约束在早期对话里。上下文压缩时摘要模型觉得这是个细节没写进摘要。结果 Agent 后期查询时把 2023 年的订单也带出来了。解决办法是压缩时对约束类信息做特殊标记强制保留。怎么识别约束类信息我的做法是在系统提示词里要求模型把用户约束单独列出来压缩时这部分不参与摘要原样保留。4.4 并行工具调用导致的竞态前面提过写操作要串行这里补充一个具体场景。Agent 要给用户 A 和用户 B 各发一封邮件如果并行执行两封邮件可能同时读取同一个模板状态导致其中一封用了错误的变量。串行执行虽然慢一点但结果可靠。判断标准很简单工具执行是否依赖共享的可变状态。依赖就串行不依赖就并行。分发层应该支持给工具打可并行标记默认串行显式标记才并行。4.5 记忆污染错误信息被当成事实记住Memory 系统如果无差别写入会把模型的错误结论也记下来。比如模型某轮误判用户想要退款这个判断被写进记忆后续所有对话都会带着这个偏见。我的做法是记忆写入前做置信度过滤只有高置信度的结论才写入长期记忆中等置信度的写入短期记忆任务结束就丢低置信度的只留在当前上下文。置信度怎么来可以让模型自己标注也可以用规则判断比如用户明确确认过的就是高置信度。常见问题表象根因解决方向循环停不下来轮次超限终止条件单一多重终止保险工具调用失败率高参数格式错缺 schema 校验接入层强校验上下文爆窗口token 超限无压缩策略分层保留摘要任务重复执行恢复后重跑状态非幂等幂等键事务后期行为跑偏约束丢失压缩误删约束强制保留成本失控token 暴涨无预算控制分层预算告警5. 关于 Harness 设计的一点个人体会搭 Harness 这件事最忌讳的是一步到位的心态。我见过太多团队想一开始就把七个子系统全做完美结果三个月过去连个能跑的原型都没有。正确的节奏是先用最糙的方式跑通闭环然后按哪个问题最痛就先解决哪个的顺序迭代。通常来说Loop 和 Context 是最先需要打磨的Guardrails 和 Observability 可以稍后Memory 和 Persistence 等业务真的需要跨会话、跨任务时再上。另一个体会是Harness 的复杂度应该和任务的复杂度匹配。如果你只是做一个查天气的 Agent七个子系统全上纯属过度设计一个 Loop 加一个工具分发就够了。但如果你要做的是能连续工作几小时、涉及几十个工具、需要人工审批的复杂 Agent那这七个一个都不能少。判断标准是任务失败一次的代价有多大。代价小就简化代价大就老老实实把每个子系统都做扎实。最后分享一个我常用的调试技巧把 Harness 的每一轮执行都当成一次独立的函数调用来看待输入是什么、输出是什么、副作用是什么。如果某一轮的输出让你觉得这不对啊就往前追溯它的输入十有八九问题出在上一轮的结果处理上。Agent 的问题很少是单点故障基本都是链式传导顺着链条往回找比盯着出错的那一轮死磕有效得多。