☰
拆解 Claude Code 的 Harness 架构:AI Agent 运行时设计与工程实践
2026/10/22 9:15:44 网站建设 项目流程

1. 从一次调试困境说起:为什么要拆解 AI Agent 的运行时

去年冬天我在做一个代码自动补全工具的时候,遇到一个很诡异的问题:同一个模型,同样的提示词,在本地跑得好好的,一放到自动化流程里就频繁出现工具调用失败、上下文丢失、甚至死循环。排查了整整两天,最后发现问题根本不在模型本身,而在于我那个简陋的“运行时”没有处理好工具调用的状态机——模型返回了一个工具调用请求,我的代码直接把它当普通文本处理了,结果模型以为工具已经执行完,继续往下编,整个链路就崩了。

这件事让我意识到一个被很多人忽略的事实:大模型能力再强,如果没有一个靠谱的运行时(Runtime)把它“托住”,它就是一个只会聊天的文本生成器。而 Claude Code 之所以能在代码任务上表现得像一个真正的 Agent,核心不在于它用了哪个模型,而在于它外面那层精心设计的 Harness 架构。

这篇文章我打算从源码结构和运行时设计的角度,把 Claude Code 的 Harness 拆开来看。适合谁读?如果你正在做 AI Agent 相关的开发,或者你只是好奇“为什么同样是调模型,别人做出来的东西能自己读文件、改代码、跑测试”,那这篇内容应该能给你一些可以直接抄作业的思路。我会尽量把每个设计决策背后的“为什么”讲清楚,而不是只罗列它有什么模块。

2. Harness 到底是什么:先把这个词说人话

2.1 从“马具”到“运行时容器”的类比

Harness 这个词直译是“马具”——套在马身上、把马的力气引导到正确方向的那套装备。马本身力气很大,但如果没有缰绳、鞍具、轭具,它就是一匹乱跑的马,拉不了车、耕不了地。AI Agent 里的 Harness 就是这个意思:模型是那匹马,Harness 是把它约束成“能干活”的那套装置。

具体到 Claude Code 这个场景,Harness 负责的事情包括:接收用户输入、组装上下文、调用模型、解析模型输出、判断是否需要调用工具、执行工具、把工具结果塞回上下文、再调用模型……这个循环一直持续到任务完成或者触发终止条件。听起来像是一个简单的 while 循环?实际上里面每一环都有大量的工程细节。

我见过不少团队做 Agent,就是把“调模型 → 解析 → 执行 → 再调模型”写成一个 for 循环,跑起来也能用,但一旦任务复杂一点就各种问题。差别就在于 Harness 的设计深度。

2.2 Harness 与 Agent、模型三者的关系

很多人会把这三个概念混在一起,我用一个具体的例子来区分。假设你让 AI 帮你“把项目里所有 console.log 删掉”:

  • 模型负责理解这句话,决定“我需要先找到所有含 console.log 的文件”,然后输出一个工具调用请求。
  • Agent是那个“有目标、会规划、能根据反馈调整策略”的整体,它知道删完之后要验证、要跑测试。
  • Harness是承载 Agent 运行的那套基础设施,它管的是:工具调用请求怎么解析、文件读取结果怎么截断、上下文超长了怎么压缩、工具执行报错了怎么反馈给模型。

模型是大脑,Agent 是行为主体,Harness 是让行为主体能稳定运转的操作系统和调度器。Claude Code 的 Harness 设计之所以值得研究,是因为它把很多“看起来应该由模型自己搞定”的事情,用确定性的代码逻辑接管了。

2.3 为什么这个架构值得单独拿出来讲

市面上讲 Agent 的文章,大部分在讲提示词工程、讲 ReAct 框架、讲怎么让模型“思考”。但真正落地的时候你会发现,决定一个 Agent 好不好用的,往往是那些不性感的工程细节:上下文怎么管理、工具结果怎么格式化、错误怎么重试、并发怎么控制。

Claude Code 的 Harness 在这些方面做了很多有意思的取舍。比如它对工具输出的截断策略、对文件读取的按需加载、对多轮对话的状态管理,都不是“教科书式”的做法,而是带着明显的实战痕迹。这些设计决策背后的思考,比架构图本身更有价值。

3. 核心模块拆解:一个 Agent 运行时需要哪些零件

3.1 输入层:不只是“接收用户消息”那么简单

表面上看,输入层就是拿到用户敲的那句话。但在 Claude Code 的 Harness 里,输入层要做的事情远不止这些。它需要处理:用户直接输入的文本、通过 @ 引用的文件内容、通过斜杠命令触发的特定行为、以及从系统层面注入的环境信息(当前目录、git 状态、项目结构等)。

这里有个关键设计:上下文不是一次性全部塞给模型的,而是分层注入的。系统提示词是一层,项目级配置是一层,当前会话历史是一层,用户这次输入附带的文件引用又是一层。每一层有不同的生命周期和优先级。为什么要这么设计?因为模型的上下文窗口是有限资源,如果把所有信息一股脑塞进去,很快就会撑爆,而且会稀释真正重要的信息。

我实测过一个对比:把项目所有文件内容直接拼进提示词,和只注入文件路径让模型按需读取,后者在大型项目上的表现明显更好。原因很简单,模型不需要“记住”所有代码,它只需要知道“去哪里找”。

3.2 上下文管理:有限窗口下的取舍艺术

上下文管理是 Harness 里最考验工程能力的部分。Claude Code 面对的场景是:一个代码任务可能涉及几十个文件、上百轮工具调用,但模型的上下文窗口就那么大。怎么办?

它的策略可以概括为“滑动窗口 + 关键信息锚定 + 按需重载”。对话历史保留最近若干轮,更早的历史会被压缩成摘要;工具调用的结果如果太长,会被截断并标注“已截断”;文件内容不常驻上下文,而是通过文件路径引用,需要时再读取。

这里有个细节值得注意:截断不是简单地从尾部砍掉。对于代码文件,它会尽量保留函数签名和结构信息,砍掉具体实现;对于命令输出,它会保留头部和尾部,中间用省略标记。这种“智能截断”比粗暴截断对模型的理解友好得多。

提示:如果你自己在做 Agent,上下文管理千万不要等到“超了再处理”。应该在每次组装上下文之前就做预算,预留出工具调用结果的空间。我踩过的坑是:模型决定调用工具时上下文还有余量,但工具返回结果特别长,直接把窗口撑爆,导致下一轮调用失败。

3.3 工具调用解析:从模型输出到可执行动作

模型输出的工具调用请求,本质上是一段结构化文本(通常是特定格式的 JSON 或 XML)。Harness 需要把它解析出来,验证参数合法性,然后路由到对应的工具执行器。

这一步的难点在于容错。模型不是每次都能输出完美格式的调用请求,有时候会多一个逗号,有时候会把参数名写错,有时候会在 JSON 外面包一层解释性文字。Claude Code 的 Harness 在这方面做了多层解析:先尝试严格解析,失败后尝试宽松解析(比如提取代码块中的 JSON),再失败就把解析错误反馈给模型让它重试。

这个“解析失败也作为一种反馈”的设计很关键。很多简易实现遇到解析失败就直接报错终止,但更好的做法是告诉模型“你刚才的输出格式有问题,请重新输出”,模型往往能自我修正。

3.4 工具执行与结果回灌:闭环的关键一环

工具执行本身不复杂,就是调用对应的函数。真正复杂的是结果怎么回灌给模型。一个文件读取工具返回的内容,需要被格式化成模型能理解的文本;一个命令执行工具返回的 stdout 和 stderr,需要被区分对待;执行超时或报错,需要被包装成结构化的错误信息。

Claude Code 在这里的设计思路是:工具结果不是原始数据,而是经过“翻译”的、带有语义标注的信息。比如文件读取结果会带上行号,命令执行结果会标注退出码,错误信息会带上错误类型。这些标注看起来是小事,但能显著降低模型理解结果的难度。

我自己的经验是,工具结果的格式化质量,直接决定了 Agent 多轮执行的稳定性。格式化做得差,模型容易误解结果,然后做出错误决策,然后你又得花更多轮次去纠正。

4. 运行时循环:Agent 的“心跳”是怎么跳的

4.1 主循环的完整生命周期

Claude Code 的运行时核心是一个循环,我把它拆成几个阶段:

  1. 组装阶段:收集当前所有需要的信息(系统提示、历史、用户输入、文件引用),组装成模型可接受的输入。
  2. 推理阶段:调用模型,获取输出。
  3. 解析阶段:判断输出是普通回复还是工具调用请求。
  4. 执行阶段:如果是工具调用,执行工具,获取结果。
  5. 回灌阶段:把工具结果格式化后加入上下文。
  6. 判断阶段:检查是否满足终止条件(任务完成、达到最大轮次、用户中断)。
  7. 循环或结束:不满足终止条件就回到组装阶段。

这个循环看起来直白,但每个阶段的边界条件处理才是真正的功夫。比如“判断阶段”怎么知道任务完成了?Claude Code 的策略是:当模型输出中不再包含工具调用请求,而是纯文本回复时,认为本轮任务结束。这个判断简单但有效。

4.2 终止条件的多重保险

一个健壮的 Agent 运行时必须有多个终止条件,否则很容易陷入死循环。Claude Code 至少考虑了这几种:

  • 自然终止:模型不再请求工具调用,输出最终回复。
  • 轮次上限:达到预设的最大循环次数,强制终止。
  • 用户中断:用户主动打断执行。
  • 错误熔断:连续多次工具执行失败或解析失败,触发熔断。
  • 资源限制:上下文窗口耗尽且无法压缩,或执行时间超限。

我特别想强调“错误熔断”这个设计。早期我做 Agent 的时候没有这个机制,结果模型陷入了一个“调用工具 → 失败 → 重试 → 再失败”的死循环,烧了一堆 token 才停下来。后来加了熔断:连续三次同类错误就终止并把错误信息返回给用户,问题就解决了。

4.3 状态管理:多轮对话中的“记忆”怎么存

Agent 的状态不只是对话历史,还包括:当前工作目录、已读取的文件缓存、已执行命令的记录、任务进度标记等。这些状态需要在多轮循环中保持一致。

Claude Code 的做法是把状态分成两类:会话状态和任务状态。会话状态是跨任务的(比如用户偏好、项目配置),任务状态是单次任务内的(比如这次任务读了哪些文件)。分开管理的好处是,任务结束后任务状态可以丢弃,不会污染后续会话。

注意:状态管理最容易出的问题是“状态泄漏”——上一个任务的状态影响了下一个任务。比如上一个任务改了某个文件,下一个任务读取时拿到了缓存里的旧内容。Claude Code 通过文件修改时间戳来校验缓存有效性,这个细节很值得借鉴。

5. 工具系统的设计哲学:少而精还是多而全

5.1 内置工具的选择逻辑

Claude Code 内置的工具数量并不多,大致是:文件读取、文件写入、文件编辑、目录列表、命令执行、搜索这几类。为什么不做一大堆工具?因为工具越多,模型的决策负担越重。每多一个工具,模型就多一个需要判断“要不要用这个”的选项,决策空间指数级增长。

它的选择逻辑是:只保留那些“高频、通用、不可替代”的工具。文件读取和命令执行几乎能覆盖所有代码任务的基础操作,其他能力可以通过命令执行来间接实现(比如用 grep 搜索而不是单独做一个搜索工具)。

这个思路和很多“工具越多越好”的做法相反,但在实际使用中,精简的工具集确实能让模型的表现更稳定。

5.2 工具描述的写法:给模型看的“说明书”

工具描述(tool description)是 Harness 里最被低估的部分。模型决定用不用一个工具、怎么用,完全依赖这段描述。Claude Code 的工具描述有几个特点:

  • 明确使用场景:不只说“这个工具能读文件”,而是说“当你需要查看文件内容时使用”。
  • 参数说明具体:每个参数的类型、是否必填、取值范围都写清楚。
  • 给出示例:好的工具描述会包含一两个调用示例。
  • 说明限制:比如“文件超过一定大小会被截断”。

我对比过写得好的工具描述和写得差的,模型调用成功率能差出一大截。写得差的时候,模型经常传错参数类型,或者在不该用的时候用。

5.3 工具结果的格式化规范

工具返回的结果需要遵循统一的格式规范,这样模型才能稳定理解。Claude Code 的规范大致是:

结果类型格式要求示例
文件内容带行号,标注文件路径1: import os
命令输出区分 stdout/stderr,标注退出码exit code: 0
错误信息标注错误类型和可操作建议Error: file not found
空结果明确说明“无结果”而非留空No matches found

这个表格看起来简单,但每一条都是踩坑踩出来的。比如“空结果要明确说明”,是因为如果返回空字符串,模型有时候会以为工具没执行,然后重复调用。

6. 上下文压缩:当窗口快满的时候怎么办

6.1 压缩策略的触发时机

上下文压缩不是等到满了才做,而是在接近阈值时就开始准备。Claude Code 的做法是设置一个“水位线”,比如窗口使用率达到 80% 时触发压缩流程。压缩的目标是腾出足够空间让循环继续,而不是把上下文压到最小。

为什么要留提前量?因为压缩本身也需要消耗 token(如果压缩是用模型做的),而且压缩后的上下文需要重新组装,这些都需要空间。

6.2 压缩的具体手段

压缩手段从轻到重有这么几层:

  • 截断工具结果:把过长的工具输出截断,保留关键部分。
  • 摘要历史对话:把较早的对话轮次用模型总结成简短摘要。
  • 丢弃中间过程:只保留任务目标和当前状态,丢弃中间的探索过程。
  • 重载文件引用:把文件内容从上下文中移除,只保留路径,需要时重新读取。

这几层是递进使用的,先尝试轻量手段,不够再上重量手段。这个递进设计很重要,因为每次压缩都会损失信息,能少损失就少损失。

6.3 压缩后的信息完整性保障

压缩最大的风险是丢失关键信息,导致模型“失忆”。Claude Code 的保障机制是:压缩时保留“锚点信息”——任务目标、已确认的关键事实、当前进度。这些锚点不参与压缩,始终保留在上下文中。

我自己的做法是维护一个“任务状态摘要”,每轮循环后更新,压缩时优先保留这个摘要。这样即使历史对话被压得很厉害,模型也能通过摘要知道“我在做什么、做到哪了”。

7. 错误处理与重试:让 Agent 不那么容易“崩”

7.1 错误分类与差异化处理

Agent 运行时的错误可以分成几类,每类需要不同的处理策略:

错误类型典型场景处理策略
模型输出格式错误JSON 解析失败反馈给模型重试
工具执行错误命令返回非零退出码把错误信息回灌给模型
参数校验错误文件路径不存在提示模型修正参数
系统级错误超时、内存不足终止或降级处理
逻辑死循环重复调用同一工具熔断并提示

差异化处理的关键是:能自我修复的错误就让模型自己修,不能修复的才终止。格式错误、参数错误通常模型能自己纠正,系统级错误和死循环则需要 Harness 强制干预。

7.2 重试的退避策略

对于可重试的错误,不能立即重试,否则容易连续失败。Claude Code 采用的是简单的退避:第一次失败后立即重试一次,再失败则把错误信息反馈给模型让它调整策略,而不是机械重试。

这个设计背后的逻辑是:Agent 场景下的重试不应该是“原样再来一次”,而应该是“带着错误信息重新决策”。因为模型每次输出可能不同,给它错误反馈后它往往会换一种方式。

7.3 熔断机制的设计

熔断是防止 Agent 失控的最后一道防线。触发条件通常是:连续 N 次同类错误、单位时间内错误率超过阈值、或者检测到重复的无效循环。

熔断后的处理不是简单报错,而是把当前状态和错误原因整理成报告返回给用户,让用户决定是继续、调整还是放弃。这个“交还控制权”的设计很重要,因为有些问题模型自己解决不了,需要人来判断。

8. 实操复现:自己搭一个简化版 Harness

8.1 最小可行架构

如果你想自己复现一个简化版的 Harness,最小架构包含这几个部分:

class SimpleHarness: def __init__(self, model_client, tools, max_turns=20): self.model = model_client self.tools = tools self.max_turns = max_turns self.history = [] self.state = {} def run(self, user_input): self.history.append({"role": "user", "content": user_input}) for turn in range(self.max_turns): context = self.build_context() response = self.model.generate(context) action = self.parse(response) if action.type == "final": return action.content result = self.execute_tool(action) self.history.append(self.format_result(result)) return "达到最大轮次限制"

这个骨架虽然简单,但已经包含了核心循环。实际落地时需要补充的是:上下文预算管理、错误处理、状态持久化。

8.2 关键参数的计算与选择

几个需要根据实际情况调整的参数:

  • max_turns:根据任务复杂度设置。简单任务 10 轮够用,复杂任务可能需要 30-50 轮。设置太小任务做不完,太大浪费资源。
  • 上下文水位线:建议设在窗口大小的 75%-85%。太低频繁压缩影响效率,太高容易撑爆。
  • 工具结果截断长度:单个工具结果建议不超过窗口的 10%-15%。这样即使连续调用多个工具也不会瞬间撑满。
  • 重试次数:同类错误重试不超过 3 次,超过就熔断。

这些参数没有标准答案,需要根据你的模型窗口大小、任务类型、工具特性来调。我的建议是先设保守值,跑一段时间后根据实际数据调整。

8.3 从简化版到生产级的差距

简化版能跑通流程,但离生产级还有距离。主要差距在:

  • 并发控制:多个工具调用怎么排队、怎么避免资源竞争。
  • 持久化:会话状态怎么存、崩溃后怎么恢复。
  • 可观测性:每轮循环的输入输出怎么记录、怎么排查问题。
  • 安全边界:工具执行的权限控制、危险操作的拦截。

这些在生产环境里都是必须的,但在学习阶段可以先跳过,先把核心循环跑通。

9. 踩坑记录:那些文档里不会写的问题

9.1 工具结果太长导致的“上下文雪崩”

这是我踩过最狠的坑。模型调用了一个命令,命令输出特别长(比如递归列目录),结果直接把上下文撑爆,下一轮调用失败,整个任务中断。后来我加了工具结果长度预检:执行前估算输出长度,超限就截断或提示模型换一种方式。

9.2 模型“假装”工具执行成功

有时候模型会在没有实际调用工具的情况下,自己编造一个工具执行结果,然后基于这个假结果继续往下做。这个问题在提示词里明确要求“必须实际调用工具”能缓解,但根治还是要靠 Harness 校验:每轮检查模型输出里是否有真实的工具调用记录。

9.3 状态不一致导致的“鬼打墙”

模型读了一个文件,改了它,然后又读了一次,但读到的是缓存里的旧内容,于是它以为改动没生效,又改了一遍。这种状态不一致问题在文件操作场景特别常见。解决办法是:文件修改后主动失效缓存,或者用文件哈希校验内容是否变化。

9.4 常见问题速查表

现象可能原因排查方向
模型重复调用同一工具工具结果没被正确理解检查结果格式化
任务中途停止上下文超限或熔断查看日志中的终止原因
工具调用参数错误工具描述不清晰优化工具描述和示例
执行结果与预期不符状态缓存过期检查缓存失效逻辑
响应越来越慢上下文膨胀检查压缩策略是否生效

10. 架构演进的一些思考

Claude Code 的 Harness 设计不是一成不变的,从它的迭代能看出一些趋势。早期版本更依赖模型自身的规划能力,Harness 只做基础的循环和工具调用;后来的版本越来越多地把确定性逻辑从模型手里接管过来,比如上下文管理、错误处理、状态维护。

这个趋势背后的逻辑是:模型擅长的是“决策”,不擅长的是“记忆”和“一致性维护”。把后者交给确定性的代码,模型就能专注于它真正擅长的事情。这也是为什么好的 Harness 能让同一个模型表现出完全不同的能力水平。

我自己在做 Agent 的过程中,最大的体会是:不要指望模型自己搞定一切。该用代码兜底的地方就用代码兜底,该给模型自由的地方就给足自由。这个边界怎么划,就是 Harness 设计的核心命题。划得好,Agent 稳定可靠;划得不好,就是各种玄学问题。

最后分享一个我常用的调试技巧:把每一轮循环的完整输入输出都落盘,出问题的时候回放整个执行链路。Agent 的问题往往不是单点故障,而是多轮累积的结果,只有看完整链路才能定位到真正的根因。这个习惯帮我省了无数排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询