资讯详情

资讯详情

AI Agent Harness 七大子系统拆解:从 Agent Loop 到 Guardrail 的工程实践

1. 拆开Harness这个词先搞清楚它到底指什么很多人第一次听到AI Agent 的 Harness这个说法第一反应是懵的——这词在传统软件工程里就有怎么突然跟 Agent 扯上关系了我一开始也绕了半天后来把两边的语境对齐了才想明白Harness 的本质是约束并驱动一个不可控核心去完成确定任务的外围装置。在测试领域它是测试夹具在硬件领域它是线束在 Agent 领域它就是包裹在 LLM 外面、让模型真正能干活的那一整套运行时骨架。这个定义很关键因为它直接决定了你搭 Agent 时该往哪个方向使劲。大模型本身是个概率函数你给它一段文字它吐一段文字仅此而已。它不会自己去看文件、不会自己记住上一轮干了什么、不会自己判断这个任务算不算完成。所有这些让它像个员工一样干活的能力全部来自 Harness。所以业内有个说法我特别认同模型决定能力上限Harness 决定能力下限。同一个模型套上不同的 Harness产出质量能差出好几倍。那 Harness 到底由哪些部分组成我翻了不少开源实现也自己从零搭过几套最后收敛出一个结论一个能真正干活的 Agent Harness拆开来就是 7 个子系统。这 7 个不是拍脑袋凑的数而是你少任何一个Agent 都会在某个环节掉链子。下面我按从外到内、从启动到收尾的顺序把这 7 个子系统一个个拆开讲每个都说清楚它解决什么问题、不写会怎样、以及实际落地时容易踩的坑。先给一张全局对照表让你有个整体印象后面再逐个展开子系统核心职责缺失后的典型症状Agent Loop驱动思考-行动-观察循环Agent 只回答一轮就停不会多步执行LLM Integration统一模型调用与输出解析换模型就崩工具调用格式对不上Tool / Skill 层把外部能力暴露给模型Agent 只能说不能做Memory / Context管理短期与长期记忆聊几轮就忘长任务上下文爆炸Planning / Orchestration任务拆解与多 Agent 编排复杂任务一步到位做一半就乱Execution Sandbox安全执行代码与命令要么不敢执行要么执行了收不回Observability / Guardrail日志、追踪、失败兜底出错完全不知道哪一步坏的这张表你先存着接下来每一节我都会把它展开成可落地的东西。2. Agent Loop整个 Harness 的心跳写不好后面全白搭2.1 为什么必须有 Loop而不是一问一答最朴素的 Agent 实现就是用户提问 → 拼 prompt → 调模型 → 返回结果。这叫 Chatbot不叫 Agent。真正的 Agent 必须能干完一件事而一件事往往需要多步。比如帮我把这个项目的测试跑通它得先看目录结构、再找测试命令、再执行、再看报错、再改、再跑——这是一个循环。Agent Loop 就是把这个循环固化下来。它的标准形态是ReAct 模式Reason推理下一步做什么→ Act调用工具→ Observe看工具返回→ 再 Reason。循环的终止条件是模型输出一个结束信号或者达到最大步数上限。我见过太多人卡在这一步写了个 while 循环但没设终止条件结果 Agent 陷入死循环疯狂调用同一个工具token 烧得飞快。所以 Loop 里有两个东西是必须的——最大迭代次数和重复动作检测。前者防止无限循环后者防止原地打转。2.2 一个能用的 Loop 骨架长什么样下面这段是我常用的最小可用 LoopPython 写的逻辑很直白def agent_loop(task, max_steps15): messages [{role: system, content: SYSTEM_PROMPT}, {role: user, content: task}] seen_actions [] for step in range(max_steps): response call_llm(messages, toolsTOOL_SCHEMAS) messages.append(response) # 模型认为任务结束 if response.finish_reason stop and not response.tool_calls: return response.content # 执行工具 for call in response.tool_calls: signature (call.name, json.dumps(call.args, sort_keysTrue)) if signature in seen_actions: messages.append({role: tool, content: 重复动作请换思路}) continue seen_actions.append(signature) result dispatch_tool(call.name, call.args) messages.append({role: tool, content: str(result)}) return 达到最大步数任务未完成这段代码看着简单但每一行都有讲究。seen_actions那个去重逻辑是我踩坑之后加的——有一次 Agent 反复读同一个文件读了 8 遍就是因为模型忘了自己读过。加了这个检测之后它收到重复动作的提示就会主动换策略。提示max_steps不要设太大。我一般设 10 到 15超过这个数还没搞定多半是任务本身描述不清或者工具不够用继续循环只是烧钱。2.3 Loop 里最容易被忽略的状态收敛判断很多人以为模型说 stop 就结束了其实不够。模型有时候会在任务没完成时就说我完成了。所以成熟的 Loop 会加一层外部校验比如任务要求生成一个文件那 Loop 结束前必须检查文件是否真的存在任务要求测试通过就得检查退出码。这个校验逻辑我通常写成一组完成条件函数在 Loop 每次准备退出时调用。校验不过就把失败原因塞回 messages让模型继续。这一招能显著降低假完成的概率实测下来任务成功率能提升不少。3. LLM Integration别把模型调用写死这是换模型的底气3.1 为什么这层要单独抽出来新手最容易犯的错是把openai.chat.completions.create(...)直接散落在业务代码里。结果想换个模型试试发现要改几十处。LLM Integration 这一层的价值就是把所有模型相关的差异收敛到一个适配器里。这些差异包括API 端点、鉴权方式、消息格式、工具调用的字段名、流式返回的解析方式、token 计费口径。不同厂商在这些细节上各有各的写法你不抽象代码就没法维护。3.2 适配器要统一哪几件事我一般会定义一个统一的接口至少覆盖这几个方法chat(messages, tools, **kwargs)同步调用返回标准化响应stream(messages, tools, **kwargs)流式调用逐块 yieldcount_tokens(text)估算 token用于上下文裁剪parse_tool_calls(response)把各家不同的工具调用格式统一成[{name, args, id}]其中parse_tool_calls是最关键的。有的模型把工具调用放在tool_calls字段有的放在function_call参数有的是 JSON 字符串有的是对象。你在适配器里统一掉上层 Loop 就完全不用关心用的是哪家模型。3.3 输出解析的容错比调用本身更重要模型输出不是永远规范的。它可能返回带 markdown 代码块的 JSON、可能多打一个逗号、可能把参数写成自然语言。所以解析层必须做容错def safe_parse_json(text): text text.strip() # 去掉 json 包裹 if text.startswith(): text re.sub(r^(?:json)?|$, , text, flagsre.M).strip() try: return json.loads(text) except json.JSONDecodeError: # 尝试提取第一个完整 JSON 对象 match re.search(r\{.*\}, text, re.S) if match: try: return json.loads(match.group()) except Exception: pass return None这个函数我几乎每个项目都会带上。没有它模型偶尔抽风返回个不规范的 JSON整个 Loop 就崩了。有了它至少能兜住大部分情况。注意容错不等于纵容。如果解析连续失败 3 次应该把错误反馈给模型让它重试而不是无限兜底。否则你会掩盖掉真正的 prompt 设计问题。4. Tool / Skill 层Agent 的手设计好坏直接决定它能干什么4.1 工具不是越多越好而是越正交越好我见过有人给 Agent 塞了 50 个工具结果模型选择困难经常调错。工具设计的核心原则是正交——每个工具职责单一工具之间不重叠。比如读文件和读文件某几行就不该是两个工具后者用参数表达即可。一个工具的定义通常包含三部分名称、描述、参数 schema。其中描述是给模型看的写得越清楚模型选得越准。我写描述有个习惯不仅说这个工具做什么还说什么时候用和什么时候不用。比如read_file: 读取指定路径的文件内容。 适用需要查看文件具体内容时。 不适用只想确认文件是否存在时请用 file_exists。这种正反说明能明显降低误调用率。4.2 工具返回值的格式决定了 Loop 的稳定性工具返回给模型的内容不是随便str(result)就完事。返回太长会撑爆上下文返回太乱模型看不懂。我的经验是结构化数据用 JSON但只保留关键字段长文本做截断并明确标注已截断共 N 行显示前 M 行错误信息要包含为什么错和可以怎么办举个例子读文件工具返回时我会带上行号因为模型后续要改文件时行号能帮它精确定位。这个细节看着小但实测能减少很多改错位置的问题。4.3 Skill 和 Tool 的区别别混为一谈热词里经常出现harness 用 skillharness creator skill这类说法。我的理解是Tool 是原子能力Skill 是面向场景的能力封装。一个 Skill 内部可能调用多个 Tool并带有自己的小流程。比如生成一份周报这个 Skill内部会依次调用读本周提交记录、读任务列表、调模型总结、写文件。对上层 Loop 来说它只看到一个generate_weekly_report的 Skill复杂度被封装了。这种分层在工具数量多的时候特别有用能让模型的决策空间保持可控。5. Memory / ContextAgent 的记性决定它能不能干长活5.1 短期记忆和长期记忆是两套机制短期记忆就是当前对话的 messages 列表它随 Loop 增长。长期记忆是跨会话的通常落到向量库或结构化存储里。很多人只做了短期结果 Agent 换个会话就失忆。短期记忆的核心矛盾是上下文窗口有限。任务一长messages 就爆。解决办法有三类滑动窗口只留最近 N 轮、摘要压缩把旧对话总结成一段、关键信息提取只保留实体和结论。我一般组合使用最近几轮保留原文更早的做摘要关键事实单独存一份。5.2 上下文裁剪的实操策略裁剪不是简单砍掉旧消息那样会丢关键信息。我的做法是分层层级内容处理方式系统层System Prompt、工具定义永不裁剪任务层原始任务描述、完成条件永不裁剪事实层提取出的关键实体、结论结构化保留对话层历史轮次摘要 滑动窗口工具层工具调用与返回保留最近 N 次旧的压缩这个分层能保证任务目标和已知事实永远在上下文里而冗长的工具输出被压缩掉。实测下来同样的任务裁剪后 token 消耗能降一半以上而成功率几乎不掉。5.3 长期记忆别乱存检索质量才是关键长期记忆常见做法是把所有对话都塞进向量库然后按相似度检索。问题是检索出来的东西经常不相关反而干扰模型。我的经验是存的时候就要做筛选只存结论性和偏好性的信息比如用户偏好用 pytest 而不是 unittest而不是把整段对话都存进去。检索时也要加过滤比如按时间、按类型。纯靠向量相似度很容易召回一堆噪音。6. Planning / Orchestration复杂任务靠拆多 Agent 靠编排6.1 单 Agent 的规划先拆再干面对帮我重构这个模块这种大任务直接让 Agent 上手它多半会乱。Planning 子系统的作用就是先把任务拆成有序的子任务再逐个执行。拆解方式有两种一种是让模型一次性输出完整计划Plan-and-Execute一种是边做边拆ReAct 的隐式规划。前者适合步骤明确的任务后者适合探索性任务。我一般先用前者生成计划执行中如果发现计划不对再动态调整。计划要落成结构化数据比如{ goal: 重构 user 模块, steps: [ {id: 1, action: 分析现有代码结构, done: false}, {id: 2, action: 识别可抽取的公共逻辑, done: false}, {id: 3, action: 逐个文件重构并跑测试, done: false} ] }有了这个结构Loop 每轮就知道当前该做哪一步不会跑偏。6.2 多 Agent 编排什么时候才需要热词里多个智能体编排很火但我要泼盆冷水大部分任务不需要多 Agent。多 Agent 的价值在于角色分工比如一个写代码、一个审查、一个测试。如果你的任务没有明显的角色差异硬上多 Agent 只会增加协调成本。真要用多 Agent编排层要解决三件事任务分发谁干什么、结果汇总怎么合并、冲突处理意见不一致听谁的。我一般用一个协调者 Agent来管这三件事其他 Agent 只负责执行自己的子任务并返回结果。6.3 编排的失败模式提前防多 Agent 最常见的失败是互相甩锅——A 说 B 没做好B 说 A 给的信息不全。防这个的办法是明确接口契约每个 Agent 的输入输出格式固定协调者只按格式校验不合格就打回。这样责任清晰不会扯皮。7. Execution Sandbox敢让 Agent 动手前提是关得住7.1 为什么执行必须隔离Agent 一旦能执行代码和命令风险就来了它可能删错文件、可能跑飞一个死循环、可能访问不该访问的路径。Sandbox 的作用就是给执行划一个圈圈内随便折腾圈外碰不到。隔离的维度有几个文件系统限制可访问目录、网络限制可访问地址、资源限制 CPU、内存、时间、进程限制能起的进程数。你不一定全上但至少要有文件系统和超时限制。7.2 超时和资源限制是保命线我踩过最惨的一次坑是 Agent 执行了一段死循环代码把机器 CPU 跑满整个服务卡死。从那以后所有执行都强制加超时import subprocess def run_safe(cmd, timeout30, cwdNone): try: result subprocess.run( cmd, shellTrue, cwdcwd, capture_outputTrue, textTrue, timeouttimeout ) return {ok: True, stdout: result.stdout[:5000], stderr: result.stderr[:2000], code: result.returncode} except subprocess.TimeoutExpired: return {ok: False, error: f执行超时{timeout}s}注意stdout我做了截断因为有些命令输出巨大直接塞回上下文会爆。截断后标注清楚模型知道信息不全会自己决定要不要再查。7.3 危险操作的二次确认有些操作不可逆比如删除文件、覆盖写入。这类操作我一般加一道确认Agent 提出要执行时先返回一个待确认状态由外部人或上层逻辑确认后才真正执行。这在自动化流程里可能显得啰嗦但能避免很多灾难性误操作。8. Observability / Guardrail出问题时你得知道坏在哪一步8.1 日志要记什么才叫能排查很多人的 Agent 日志只记了最终结果出错了完全不知道中间发生了什么。合格的日志应该记录每一步的输入输出第几轮、模型返回了什么、调了哪个工具、参数是什么、返回是什么、耗时多少。我一般用结构化日志每条一个 JSON方便后续检索def log_step(step, event_type, payload): logger.info(json.dumps({ step: step, type: event_type, # llm_call / tool_call / tool_result payload: payload, ts: time.time() }, ensure_asciiFalse))有了这个出问题时我能完整回放整个执行链路定位到具体哪一步坏了。8.2 Guardrail给 Agent 划红线Guardrail 是一组硬规则在模型输出之后、执行之前做校验。比如输出里包含敏感路径拦截单次工具调用参数超过阈值拦截连续失败超过 N 次终止任务这些规则不依赖模型判断是确定性的所以可靠。模型可以被 prompt 影响但 Guardrail 不会。两者配合才能既灵活又安全。8.3 失败兜底任务失败也要优雅Agent 任务失败是常态关键是失败时要有清晰的收尾告诉用户失败在哪一步、已经完成了什么、建议下一步怎么办。我一般会在 Loop 退出时生成一份执行报告包含已完成步骤、失败原因、残留状态。这份报告比一句任务失败有用得多。9. 把这 7 个子系统串起来一个最小可用的组装顺序讲了这么多最后说说实际搭建时的顺序。我的建议是从中间往外搭先写 LLM Integration保证能稳定调模型、解析输出再写 Agent Loop用最简单的工具跑通多步执行然后补 Tool 层把你要的能力一个个加进去接着加 Memory解决上下文爆炸任务复杂了再上 Planning要执行代码了加 Sandbox最后补 Observability 和 Guardrail让系统可维护这个顺序的好处是每一步都能跑起来、能验证不会一上来就搭个大框架结果处处报错。我个人在实际操作中的体会是Harness 的难点从来不在某个子系统本身而在它们之间的衔接。比如 Loop 怎么把工具返回喂回模型、Memory 怎么和 Planning 共享状态、Sandbox 的错误怎么被 Guardrail 捕获。这些衔接点才是真正花时间的地方也是区分能跑和好用的分水岭。搭的时候多花点心思在这些边界上后面维护会轻松很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →