
1. 先搞清楚 Harness 到底指什么很多人第一次听到 AI Agent 的 Harness脑子里浮现的是测试框架里那个 harness或者硬件调试用的线束。但在 Agent 语境下Harness 的含义完全不同——它是让大模型真正“下地干活”的那套工程骨架。你可以把 LLM 想象成一个脑子很聪明但手脚被绑住的专家它只能输出文字没法读文件、发请求、跑命令、记住上次干了什么。Harness 就是给这个专家解开绳子、递上工具、装上记忆、配好安全绳的那一整套装置。我接触过不少团队模型选得很激进Prompt 写得也漂亮Demo 阶段惊艳全场一上真实任务就崩。问题几乎从来不在模型本身而在于 Harness 没搭好。模型再强如果工具调用协议设计得含糊、上下文管理一塌糊涂、错误恢复全靠重试那它就是个只会聊天的玩具。反过来一个中等能力的模型配上扎实的 Harness在限定场景里能稳定干出让人满意的活。所以这篇文章要拆的就是 Harness 内部到底由哪些子系统组成每个子系统解决什么问题它们之间怎么咬合以及从零搭建时哪些坑必须提前避开。适合正在做 AI Agent 落地、被“Demo 很美好上线就翻车”折磨过的开发者也适合想理解 Agent 工程本质的产品和技术负责人。下面这 7 个子系统是我在实际项目中反复验证后总结出来的最小完备集合缺一个都会在某个阶段卡住你。2. 七个核心子系统的整体拆解2.1 为什么是这七个而不是更多或更少先说结论Agent Loop、LLM Integration、Tool System、Context Manager、Memory Store、Guardrail、Observability。这七个不是拍脑袋定的而是按照“一个 Agent 从收到任务到交付结果”的完整生命周期倒推出来的。任务进来要有循环驱动循环里要调模型模型要能操作外部世界操作需要上下文跨轮次需要记忆行为需要边界出问题需要能看见。每一环都对应一个不可省略的职责。有人会问Guardrail 和 Observability 是不是可以合并进别的模块我的经验是不要。安全边界和可观测性在早期看起来像“锦上添花”但一旦 Agent 开始执行有副作用的操作——写数据库、发消息、调支付——这两块就是保命的。把它们独立成子系统是为了在架构上强制留出位置而不是等到出事再补。还有人会加一个 Planning 子系统。我的看法是规划能力可以放在 LLM Integration 里通过 Prompt 和结构化输出实现也可以放在 Agent Loop 里作为循环的一个阶段不一定非要独立。过早拆出 Planning 反而会让架构变重调试链路变长。等你的 Agent 确实需要多步复杂规划时再拆不迟。2.2 子系统之间的数据流与依赖关系理解这七个系统的关键不是记住名字而是看清它们怎么串起来。一次典型的 Agent 执行是这样的Agent Loop 启动从 Context Manager 拿到当前上下文把上下文和可用工具列表交给 LLM Integration模型返回一个工具调用请求Tool System 执行这个调用并把结果写回 Context Manager同时 Memory Store 决定这次交互里哪些信息值得长期保留Guardrail 在工具执行前后各检查一次Observability 全程记录每一步的输入输出和耗时。这里有个容易被忽视的点Context Manager 和 Memory Store 的边界。Context Manager 管的是“这一次任务执行期间”的上下文窗口它要解决的是 token 预算和内容裁剪Memory Store 管的是“跨任务、跨会话”的持久化信息它要解决的是检索和写入策略。两者混在一起做最后一定是又慢又乱。我见过把对话历史全塞进向量库当记忆用的方案检索出来的东西和当前任务八竿子打不着模型被带偏得厉害。依赖关系上Agent Loop 是调度中枢它依赖其余六个LLM Integration 依赖 Context Manager 提供输入Tool System 依赖 Guardrail 做前置校验Observability 是横切关注点被所有子系统调用。画成图是一张网但主链路是清晰的。3. Agent Loop 与 LLM Integration 的实操要点3.1 Agent Loop 的循环终止条件设计Agent Loop 看起来简单——不就是 while 循环调模型吗但真正难的是终止条件。我踩过的最大的坑是只设了“模型不再调用工具就结束”这一个条件。结果遇到模型陷入“调用工具→看结果→再调用同一个工具”的死循环跑了四十多轮才因为超时被杀掉账单直接起飞。一个健壮的终止条件至少要有四层。第一层是正常终止模型返回的响应里没有工具调用只有最终答案。第二层是轮次上限硬性限制最大循环次数我一般设 15 到 25 轮具体看任务复杂度。第三层是重复检测如果连续三轮调用了相同的工具且参数高度相似强制中断并让模型基于已有信息作答。第四层是预算控制累计 token 消耗或累计工具调用次数超过阈值就停。MAX_TURNS 20 MAX_TOKENS 100000 seen_calls [] for turn in range(MAX_TURNS): response llm.invoke(context) if not response.tool_calls: return response.content call_signature (response.tool_calls[0].name, str(response.tool_calls[0].args)) if call_signature in seen_calls[-3:]: context.append(system_msg(检测到重复调用请基于现有信息作答)) continue seen_calls.append(call_signature) result execute_tool(response.tool_calls[0]) context.append(result)注意轮次上限不要设得太小。有些任务确实需要多步工具调用才能完成设成 5 轮会让 Agent 频繁半途而废。我的经验值是先设 20观察实际任务的轮次分布后再调整。3.2 LLM Integration 的工具调用协议LLM Integration 的核心职责有两个把上下文和工具描述组装成模型能理解的输入以及解析模型的输出判断它是想说话还是想调工具。现在主流模型都支持结构化的 function calling但不同厂商的格式有差异这层要做适配。工具描述的质量直接决定模型会不会正确调用。我见过把工具描述写成“查询数据”四个字的模型根本不知道什么时候该用、参数怎么填。好的工具描述要包含这个工具做什么、什么时候用、每个参数的含义和格式、返回什么。这本质上是在给模型写文档文档质量就是调用准确率。{ name: search_orders, description: 根据用户ID和日期范围查询订单列表。当用户询问历史订单、消费记录时使用。, parameters: { user_id: {type: string, description: 用户唯一标识格式为 U 开头加数字}, start_date: {type: string, description: 起始日期格式 YYYY-MM-DD}, end_date: {type: string, description: 结束日期格式 YYYY-MM-DD} } }另一个实操要点是并行工具调用。有些模型支持一次返回多个工具调用请求如果你的 Tool System 不支持并行执行要么串行跑慢要么只取第一个丢信息。我建议在 Tool System 层面支持并行但要注意工具之间是否有依赖——有依赖的必须串行。3.3 模型输出解析的容错处理模型不是每次都能返回完美的结构化输出。它可能返回带 markdown 代码块包裹的 JSON可能在 JSON 前后加解释文字可能字段名拼错。解析层必须做容错不能一遇到格式问题就抛异常。我的做法是三级解析先尝试直接 JSON 解析失败则用正则提取代码块内容再解析再失败则把原始输出和解析错误一起返回给模型让它重新生成。第三级很关键很多团队到这里就放弃了其实让模型自己修格式的成功率相当高。4. Tool System 与 Guardrail 的配合4.1 工具注册与发现机制Tool System 要解决的第一件事是工具怎么注册、模型怎么知道有哪些工具可用。最朴素的做法是维护一个全局字典启动时把所有工具塞进去。但工具一多全量塞给模型会撑爆上下文而且模型在几十个工具里选准确率会明显下降。更好的做法是分组加动态加载。按业务域把工具分成若干组根据当前任务类型只加载相关组的工具。比如用户问订单问题就只加载订单查询、退款、物流这几个工具不加载用户管理、报表导出那些。这样模型的选择空间小了准确率自然上去。TOOL_GROUPS { order: [search_orders, get_order_detail, request_refund], logistics: [track_shipment, estimate_delivery], account: [get_profile, update_contact] } def load_tools(task_type): return TOOL_GROUPS.get(task_type, [])工具的执行要有超时和重试。外部 API 调用可能慢、可能失败不能让 Agent Loop 卡死。我给每个工具设 10 秒超时失败后最多重试两次重试仍失败就把错误信息返回给模型让它决定是换个方式还是告知用户。4.2 Guardrail 的三道防线Guardrail 不是一道墙而是三道。第一道在工具执行前检查这次调用是否被允许——参数是否合法、操作是否有权限、是否触发了频率限制。第二道在工具执行中监控执行过程是否有异常行为比如某个工具突然要访问大量数据。第三道在工具执行后检查返回结果是否包含敏感信息是否需要脱敏再交给模型。参数校验是最基础也最容易被跳过的一环。模型可能生成格式正确但语义荒谬的参数比如查询日期传了个 2099 年或者用户 ID 传了个不存在的值。这些在工具执行前就该拦下来返回明确的错误让模型修正而不是让工具去处理脏数据。提示Guardrail 的拒绝信息要写得具体。“操作被拒绝”这种话模型没法据此调整要写成“日期范围超出允许查询的最近 12 个月请调整后重试”。模型拿到具体原因修正的成功率会高很多。4.3 危险操作的二次确认有些操作是不可逆的——删除数据、发送消息、发起支付。这类操作不能模型说执行就执行。我的做法是引入确认机制当模型请求执行危险操作时Harness 不直接执行而是把操作详情呈现给用户等用户确认后再执行。这个机制在实现上要注意确认请求要包含足够的信息让用户判断——要删什么、要发给谁、金额多少。信息不全的确认等于没确认用户只能盲目点同意。另外确认要有超时用户长时间不响应就取消操作避免任务挂起。5. Context Manager 与 Memory Store 的分工5.1 上下文窗口的预算分配Context Manager 最核心的工作是 token 预算管理。模型的上下文窗口是有限的而 Agent 执行过程中会不断产生新内容——工具返回结果、模型思考过程、用户新输入。如果不加控制很快就会超限。我的分配策略是这样的系统提示词占 10%工具描述占 15%长期记忆检索结果占 15%最近对话历史占 40%当前工具返回结果占 20%。这个比例不是固定的要根据任务类型调整。工具调用密集的任务工具描述和返回结果的比例要调高纯对话任务对话历史的比例可以调高。当预算不够时裁剪顺序是从最不重要的开始。工具返回结果如果太长先做摘要再放入对话历史优先保留最近的早期的做压缩长期记忆只保留相关度最高的几条。裁剪的原则是保住当前任务完成所需的最小信息集。5.2 记忆的写入与检索策略Memory Store 要回答两个问题什么信息值得记以及怎么在需要时找回来。写入策略上我倾向于“宁缺毋滥”。不是每轮对话都值得存只有包含用户偏好、重要事实、任务结论的内容才写入。比如用户说“我以后都用中文回复”这是偏好要记用户问“今天天气怎么样”这是临时信息不记。检索策略上纯向量相似度检索经常召回不相关的内容。我的做法是混合检索向量相似度加关键词匹配加时间衰减。时间衰减很重要三个月前的偏好可能已经过时了权重应该降低。def retrieve_memory(query, top_k5): vector_results vector_search(query, top_k20) keyword_results keyword_search(query, top_k20) merged merge_and_dedupe(vector_results, keyword_results) scored [(m, m.score * time_decay(m.timestamp)) for m in merged] return sorted(scored, keylambda x: x[1], reverseTrue)[:top_k]记忆还要有更新和删除机制。用户改了偏好旧的要失效用户要求删除某些信息要能真正删掉。没有这些机制记忆库会越来越脏检索质量越来越差。5.3 长对话的压缩技巧长对话是 Context Manager 的噩梦。几十轮对话下来原始历史根本放不下。压缩是必须的但压缩会丢信息怎么压是关键。我的做法是分层压缩。最近 5 轮保留原文5 到 15 轮做要点摘要15 轮以上只保留结论和关键事实。摘要用模型来做但要给明确的指令保留用户意图、已确认的事实、未完成的任务丢弃寒暄和重复内容。def compress_history(history): recent history[-5:] middle summarize(history[-15:-5], 提取用户意图和已确认事实200字以内) old extract_conclusions(history[:-15]) return old middle recent压缩后的内容要标注来源轮次方便追溯。有时候模型基于压缩后的信息做了错误判断需要回查原始对话没有来源标注就查不了。6. Observability 与常见问题排查6.1 必须记录的观测数据Observability 不是可选项。Agent 的行为是概率性的同一个输入两次执行可能走不同路径没有完整的观测数据出了问题根本没法排查。必须记录的数据包括每次模型调用的完整输入输出、每次工具调用的参数和结果、每轮循环的耗时和 token 消耗、Guardrail 的拦截记录、记忆的读写记录。这些数据要能按任务 ID 串起来。一个任务从开始到结束所有相关的调用记录要能一键拉出来。我见过只记日志不记关联 ID 的系统排查问题时要在海量日志里人肉搜索效率极低。class TraceContext: def __init__(self, task_id): self.task_id task_id self.events [] def record(self, event_type, data): self.events.append({ task_id: self.task_id, type: event_type, data: data, timestamp: time.time() })6.2 典型故障的排查路径Agent 出问题症状往往很模糊——“它不干活了”“它答非所问”“它一直转圈”。排查要从观测数据入手逐层定位。症状可能原因排查方向一直循环不结束终止条件失效或工具返回无意义结果看循环轮次和重复调用记录答非所问上下文被污染或记忆召回错误看实际送入模型的上下文内容工具调用失败率高工具描述不清或参数校验过严看失败调用的参数和错误信息响应越来越慢上下文膨胀或记忆检索变慢看每轮 token 数和检索耗时结果不稳定模型温度过高或缺少确定性约束看同输入多次执行的差异排查的核心方法是“还原现场”。把出问题那次执行的完整上下文、工具调用序列、模型输出都拉出来一步步看是哪一环偏了。大部分问题在还原现场后都能定位。6.3 性能与成本的平衡Agent 的成本主要来自模型调用。轮次越多、上下文越长成本越高。优化成本不是简单地砍轮次或砍上下文而是提高每一轮的信息密度。我的几个实操技巧工具返回结果做预处理只把模型需要的信息给它不要把整个 API 响应塞进去系统提示词精简去掉所有不影响行为的客套话简单任务用便宜模型复杂任务才上强模型可以在 Agent Loop 里做模型路由。def route_model(task_complexity): if task_complexity simple: return cheap_model elif task_complexity medium: return standard_model else: return powerful_model延迟优化上工具并行执行是最大的收益点。如果模型一次请求了三个独立工具串行跑要三倍时间并行跑只要一倍。另外模型调用的流式输出要做好让用户能尽早看到进展感知延迟会低很多。7. 从零搭建时的几个关键决策7.1 自研还是用框架这是每个团队都会纠结的问题。我的建议是如果你的 Agent 逻辑简单、工具少、场景固定用现成框架快速起步没问题。但如果你要做的是核心业务系统Agent 行为需要精细控制那 Harness 的关键部分建议自研。原因在于框架为了通用性会做很多抽象这些抽象在简单场景下是便利在复杂场景下是束缚。比如你想自定义上下文裁剪策略、想精细控制工具加载、想实现特殊的记忆检索逻辑框架往往不给你这个口子或者改起来很别扭。自研 Harness 的七个子系统每个都可以按你的业务特点来设计长期看维护成本反而更低。折中方案是用框架做原型验证验证通过后把核心逻辑逐步替换成自研。这样既快速拿到了反馈又保留了后续优化的空间。7.2 子系统拆分的粒度七个子系统是逻辑划分不代表要拆成七个微服务。早期阶段我强烈建议单体部署七个模块在一个进程里通过清晰的接口调用。这样调试方便链路短出问题好定位。等到某个子系统成为瓶颈——比如记忆检索拖慢了整体响应或者工具执行需要独立扩缩容——再把它拆出去。过早微服务化会让你的调试成本指数级上升一个请求跨五个服务日志都串不起来。7.3 测试策略的特殊性Agent 的测试和传统软件测试不一样。传统测试是确定性的输入 A 必然输出 B。Agent 是概率性的同一个输入可能走不同路径。所以测试策略要调整。我的做法是三层测试。第一层是单元测试测各个子系统的确定性逻辑比如参数校验、上下文裁剪、记忆检索排序。第二层是场景测试给定任务描述跑多次看成功率关注的是“能不能完成”而不是“怎么完成”。第三层是回归测试把线上出过问题的案例收集起来每次改动后都跑一遍确保不重复踩坑。注意场景测试的成功率阈值不要设成 100%。Agent 有随机性要求 100% 通过会导致测试极其脆弱。我一般设 90% 到 95%低于这个值才认为有问题。7.4 上线后的持续迭代Agent 上线不是终点。真实用户的输入分布和测试时完全不一样会冒出各种边界情况。Observability 收集的数据就是迭代的燃料。我会定期分析几类数据失败任务的共同特征、高频的工具调用失败原因、用户反馈不好的交互模式。针对性地调整工具描述、优化上下文策略、补充 Guardrail 规则。这个迭代过程是持续的没有一劳永逸的配置。最后分享一个我踩过的坑不要在没有 Observability 的情况下上线 Agent。我曾经在一个内部工具上偷懒觉得用户少、问题好排查结果上线第二天就遇到一个诡异的行为没有完整日志花了整整一天才定位到是记忆检索把两条不相关的记录混在了一起。从那以后Observability 永远是我搭建 Harness 时第一个完成的子系统。