ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Coding Agent Harness 架构设计:从 ZCode 7.3k Star 拆解工程化实践

Coding Agent Harness 架构设计:从 ZCode 7.3k Star 拆解工程化实践

Coding Agent 这个方向在过去一年里从"玩具级 demo"迅速卷成了"工程级基础设施",而 ZCode 能在 GitHub 上攒到 7.3k Star,靠的不是模型能力本身,而是它把Coding Agent Harness这层"脚手架"做成了可复用、可插拔、可观测的架构。很多人第一次听到 Harness 这个词会懵——它既不是模型,也不是 IDE 插件,更不是简单的 CLI 包装。它更像是给一个"会写代码但不太靠谱的实习生"配的一整套工位:工具台、权限门禁、记忆本、验收流程、日志审计,一样都不能少。这篇就围绕 ZCode 这类 Coding Agent Harness 的架构该怎么搭,把我自己踩过的坑、做过的取舍、以及那些文档里不会写的细节,一次性讲透。不管你是想读懂 ZCode 源码,还是准备自己从零搭一套,或者只是想知道"为什么我的 Agent 老是跑飞",下面的内容都能直接拿去对照。

1. 先把 Harness 的边界划清楚,别一上来就堆功能

1.1 Harness 到底管什么,不管什么

搭 Harness 最容易犯的错,就是把它当成"万能胶"——模型的事、工具的事、UI 的事全往里塞。结果代码越写越乱,最后连自己都说不清哪层该负责什么。我的经验是:Harness 的核心职责只有四件事——编排循环、工具调度、上下文管理、安全约束。模型推理本身不归它管,具体某个工具怎么实现也不归它管,它只负责"什么时候调谁、调完怎么处理、出错怎么办"。

拿 ZCode 的定位来类比,它更像是一个Agent Runtime:接收用户任务,决定下一步动作,调用工具,把结果喂回模型,循环直到任务完成或触发终止条件。它不生产智能,它只是智能的调度中枢。这个边界一旦模糊,后面所有架构决策都会跟着歪。

我见过一个团队,把文件 diff 渲染、语法高亮、甚至终端 UI 全塞进 Harness 主循环里,结果每次改 UI 都要动核心逻辑,测试覆盖率直接崩盘。正确的做法是:Harness 只暴露事件和状态,UI 层订阅这些事件自己渲染。关注点分离不是口号,是保命符。

1.2 为什么"循环"是 Harness 的心脏

Coding Agent 和普通 Chatbot 最大的区别,在于它是多轮工具调用循环,而不是一问一答。一个典型循环长这样:

  1. 组装当前上下文(系统提示 + 历史 + 工具定义 + 环境信息)
  2. 调用模型,拿到输出
  3. 解析输出,判断是"直接回答"还是"要调工具"
  4. 如果要调工具,执行工具,把结果追加到上下文
  5. 回到第 1 步,直到模型给出终止信号或达到最大轮数

这个循环看似简单,但每一个环节都藏着坑。比如第 3 步的解析,模型可能返回格式错误的 JSON、可能一次想调多个工具、可能在工具调用里夹带自然语言解释。第 5 步的终止条件,如果只靠模型自己说"我完成了",那它大概率会在没真正完成时就宣布胜利。

提示:循环一定要有硬性终止条件——最大轮数、最大 token 消耗、最大墙钟时间,三者至少设两个。我吃过亏,一个死循环跑了 40 分钟烧掉一堆额度,就因为模型在两个工具之间反复横跳。

1.3 一个最小可用的 Harness 骨架

在动手写复杂功能前,先把最小闭环跑通。下面是我常用的骨架结构,伪代码级别,但逻辑完整:

class Harness: def __init__(self, model, tools, max_turns=20, max_tokens=100000): self.model = model self.tools = {t.name: t for t in tools} self.max_turns = max_turns self.max_tokens = max_tokens self.context = ContextManager() def run(self, task): self.context.add_user(task) for turn in range(self.max_turns): prompt = self.context.build(self.tools) response = self.model.invoke(prompt) self.context.add_assistant(response) if response.is_final: return response.content for call in response.tool_calls: result = self.execute(call) self.context.add_tool_result(call.id, result) raise MaxTurnsExceeded()

这个骨架里,ContextManager和execute是两个最需要打磨的地方,后面会单独展开。先把循环跑通,再谈优化,别本末倒置。

2. 工具层设计:Agent 的手脚,决定了它的上限

2.1 工具不是越多越好,而是越"正交"越好

新手搭 Harness 时,恨不得把能想到的工具全塞进去:读文件、写文件、跑命令、搜网页、查数据库、发请求……结果模型面对几十个工具直接选择困难,调用准确率暴跌。我的原则是:工具之间职责正交,宁可组合,不要重叠。

比如"读文件"和"搜索文件内容"是两个正交工具,但"读文件"和"读文件某几行"就不该拆成两个——后者用参数表达即可。ZCode 这类项目通常会把工具控制在 10 个以内,每个工具的参数设计得足够灵活,靠组合覆盖大部分场景。

工具类别典型工具设计要点
文件操作read_file, write_file, list_dir路径必须做沙箱校验
命令执行run_shell超时、白名单、输出截断
检索grep, search结果条数上限,避免撑爆上下文
网络fetch_url域名白名单,响应体大小限制
任务管理todo_write让 Agent 自己维护计划

2.2 工具描述就是给模型的"说明书",别糊弄

模型选不选对工具,八成取决于工具描述写得好不好。我见过太多项目,工具描述就一行"读取文件",参数说明也含糊,然后抱怨模型老调错。工具描述要像写给一个聪明但完全不了解你系统的同事看:这个工具干什么、什么时候用、什么时候别用、参数什么含义、返回什么格式、有什么副作用。

举个例子,run_shell的描述我会这么写:

在受控环境中执行 shell 命令并返回标准输出和标准错误。 适用场景:运行测试、构建项目、执行 git 操作。 不适用:读取文件内容(用 read_file)、搜索代码(用 grep)。 注意:命令有 30 秒超时,输出超过 10000 字符会被截断。 危险命令(如 rm -rf)会被拦截并返回错误。

这段描述里,"不适用"和"注意"两段是关键,它们直接减少了模型的误用。实测下来,光是把工具描述写细,工具调用准确率能提升 20% 以上。

2.3 工具执行必须"可中断、可超时、可回滚"

工具执行是 Harness 里最容易出事故的地方。一个run_shell卡死,整个 Agent 就挂那了。所以执行层必须做到三点:

  • 可中断:每个工具执行跑在独立线程或进程里,主循环能发取消信号
  • 可超时:每个工具配独立的超时时间,命令类 30 秒,网络类 15 秒,文件类 5 秒
  • 可回滚:写文件、改配置这类有副作用的操作,执行前先备份,失败时恢复

我踩过最惨的坑,是一个 Agent 在调试时把项目的.env文件覆盖了,因为它"觉得"需要重置配置。从那以后,所有写操作我都加了备份机制,并且对敏感路径做了硬性拦截。

注意:工具执行结果一定要做大小截断。模型上下文是有限资源,一个cat大文件就能把窗口撑爆。我的做法是超过阈值就截断,并在结果里明确标注"已截断,共 N 行,显示前 M 行"。

3. 上下文管理:Harness 里最容易被低估的硬骨头

3.1 上下文不是"历史记录",是"工作记忆"

很多人把上下文管理简单理解成"把对话历史拼起来",这是大错特错。Coding Agent 的上下文更像人的工作记忆:当前任务是什么、已经做了哪些尝试、哪些路走不通、当前文件状态如何、下一步计划是什么。这些信息需要被主动组织和压缩,而不是无脑堆砌。

一个典型的长任务,跑几十轮之后历史会长到几万 token。如果全量塞给模型,不仅贵,而且模型会"迷失在中间"——开头和结尾的信息记得住,中间的全忘了。所以上下文管理要解决的核心问题是:在有限窗口里,保留最相关的信息。

3.2 分层上下文:把信息按"保质期"分类

我的做法是把上下文分成四层,每层有不同的更新和淘汰策略:

层级内容更新频率淘汰策略
系统层角色定义、行为准则、工具说明固定不淘汰
任务层用户目标、当前计划、待办列表每轮可能更新始终保留
工作层最近 N 轮的工具调用与结果每轮更新滑动窗口
归档层早期历史、已完成子任务摘要定期压缩摘要化

关键是归档层:当工作层超过阈值,就把最老的一批交互压缩成一段摘要,比如"已读取 config.py,发现数据库配置在第 12 行;已运行测试,3 个失败"。这样既保留了关键结论,又释放了窗口空间。

3.3 压缩不是删减,是"提炼决策依据"

压缩上下文时,最容易犯的错是简单粗暴地删掉旧消息。但旧消息里往往藏着"为什么这么做"的决策依据,删了之后模型可能重复踩坑。所以压缩要保留的是结论和理由,而不是原始的工具输出。

我常用的压缩提示词逻辑是这样的:

把以下交互历史压缩成简洁摘要,保留: 1. 已确认的事实(文件路径、配置值、错误信息) 2. 已做出的决策及原因 3. 未解决的问题和下一步计划 丢弃:完整的工具输出、重复的尝试、无关的闲聊

实测下来,一段 5000 token 的历史能压到 300 token 左右,信息保留率在 90% 以上。这个压缩动作本身也要调模型,所以别太频繁,一般工作层超过窗口 60% 时触发一次。

3.4 文件状态追踪:别让 Agent 活在"幻觉"里

Coding Agent 有个隐蔽的坑:它读过的文件内容会留在上下文里,但文件可能已经被它自己或外部改过了。如果它基于旧内容做决策,就会产生"幻觉式修改"。解决办法是维护一份文件状态表:记录每个被读写过的文件的路径、最后修改时间、内容哈希。每次要基于某文件操作前,先校验状态是否新鲜,不新鲜就重新读。

这个机制听起来简单,但能省掉大量"改了又改还是不对"的调试时间。我在一个重构任务里加了这个机制后,Agent 的无效重试次数直接降了一半。

4. 安全与权限:Harness 的刹车系统,不能省

4.1 为什么 Agent 比普通程序更需要权限控制

普通程序的行为是开发者写死的,而 Agent 的行为是模型动态生成的。这意味着你无法预先枚举它所有可能的操作。一个没约束的 Agent,理论上可以删库、可以外发数据、可以改系统配置。所以 Harness 必须有一套运行时权限系统,在工具执行前做拦截。

ZCode 这类项目通常采用"默认拒绝 + 显式授权"的策略:所有危险操作默认拦截,需要用户确认或配置白名单才放行。这个思路和移动端的权限模型很像——App 想用相机,得先申请。

4.2 权限粒度:按工具、按参数、按路径

权限控制不能只做到"工具级",那样太粗。比如run_shell允许执行,不代表允许执行rm -rf /。所以权限要下沉到参数级:

  • 命令白名单:只允许ls、cat、grep、git status等只读或安全命令
  • 路径沙箱:所有文件操作限制在项目根目录内,../一律拒绝
  • 网络域名白名单:只允许访问指定域名,防止数据外泄
  • 写操作二次确认:修改关键文件前弹确认

我一般会把这些规则写成一个独立的PolicyEngine,工具执行前统一过一遍。规则用配置表达,方便调整,别硬编码在业务逻辑里。

4.3 危险操作的"软着陆"设计

直接拒绝危险操作有时会打断正常流程。更好的做法是软着陆:拦截后不是简单报错,而是给模型一个可操作的反馈。比如模型想执行rm -rf build/,Harness 拦截并返回:"该命令被安全策略拦截。如需清理构建产物,请使用make clean或明确指定允许删除的路径。"

这样模型能理解为什么被拦,并尝试替代方案,而不是卡死或反复重试同一个被拒命令。这个细节很小,但对 Agent 的"自主性"体验影响很大。

提示:权限日志一定要留。每次拦截、每次授权都记下来,出问题时这是唯一的追溯依据。我习惯把日志按会话分文件存,方便事后复盘。

5. 可观测性:没有日志和追踪,Harness 就是个黑盒

5.1 Agent 调试为什么比普通程序难

普通程序出错,堆栈一打,问题基本定位。Agent 出错,你面对的是"模型在某一轮基于某个上下文做了某个决策",中间任何一环都可能出问题。没有完善的追踪,你根本不知道是提示词的问题、工具描述的问题、还是上下文压缩丢了关键信息。

所以 Harness 从第一天就要把可观测性做进去,而不是等出问题再补。核心是三样东西:结构化日志、调用链追踪、指标统计。

5.2 结构化日志:记录"决策现场"

日志不能只记"调用了工具 X",要记清楚决策上下文:这一轮的输入是什么、模型输出了什么、为什么选了这个工具、工具返回了什么、耗时多少。我通常用 JSON 格式记录,每条日志包含:

{ "session_id": "abc123", "turn": 7, "timestamp": "2024-01-15T10:23:45Z", "model_input_tokens": 4200, "model_output": {"type": "tool_call", "tool": "read_file", "args": {"path": "src/main.py"}}, "tool_result": {"status": "success", "bytes": 1024, "truncated": false}, "duration_ms": 340 }

有了这些,回放一个失败会话就像看录像一样清楚。我排查过一个"Agent 反复读同一个文件"的问题,就是靠日志发现上下文压缩把"已读过该文件"的结论给压没了。

5.3 指标统计:用数据发现系统性问题

单看一次会话看不出趋势,但把指标聚合起来,问题就浮出来了。我常盯的几个指标:

  • 平均轮数:突然升高说明任务变复杂或循环有问题
  • 工具调用成功率:低于 90% 说明工具描述或参数设计有问题
  • 上下文压缩频率:过高说明任务太长或压缩策略太激进
  • token 消耗分布:定位哪个环节最烧钱

这些指标不需要多复杂的系统,一个简单的统计脚本加个看板就够。关键是持续看,而不是出事才看。

5.4 会话回放:把"黑盒"变成"白盒"

如果条件允许,做一个会话回放功能:把一次完整会话的所有轮次、输入输出、工具调用按时间线展示出来。这个功能在调试复杂任务时价值巨大,你能直观看到 Agent 的"思考过程"在哪里跑偏。ZCode 这类项目通常会在 CLI 里提供 verbose 模式,本质就是简化版的回放。

6. 从 ZCode 这类项目里能抄到的架构经验

6.1 插件化:让 Harness 能"长出新手脚"

ZCode 能攒到 7.3k Star,一个关键设计是工具和模型的插件化。Harness 核心保持稳定,工具、模型、甚至上下文策略都通过接口注册进来。这样社区能贡献各种工具,而不用改核心代码。

插件化的关键是接口要稳定且最小。一个工具插件只需要实现name、description、parameters、execute四样东西,剩下的调度、权限、日志全由 Harness 统一处理。接口越小,插件越好写,生态越容易起来。

6.2 配置驱动:把策略从代码里赶出去

哪些工具启用、权限规则怎么定、上下文窗口多大、压缩阈值多少——这些都不该硬编码。ZCode 这类项目一般用一份配置文件(YAML 或 JSON)管理所有策略,启动时加载。好处是:换场景不用改代码,调参不用重新编译,团队协作时配置能进版本控制。

我自己的项目里,配置文件大概长这样:

harness: max_turns: 30 max_tokens: 150000 context: window_ratio: 0.6 compression_model: "fast" tools: enabled: [read_file, write_file, grep, run_shell] run_shell: timeout: 30 whitelist: ["ls", "cat", "grep", "git", "npm", "pytest"] policy: sandbox_root: "./workspace" network_whitelist: ["api.github.com"]

6.3 渐进式增强:先跑通,再优化

看 ZCode 的迭代路径,能明显感觉到它是先保证核心循环可用,再逐步加工具、加权限、加可观测性。这个顺序很重要。新手常犯的错是一上来就追求"完美架构",结果核心循环还没跑通,就在纠结插件系统怎么设计。

我的建议是分三阶段:

  1. MVP 阶段:单模型 + 3 个核心工具(读、写、跑命令)+ 简单循环,先让它能完成一个真实小任务
  2. 稳定阶段:加权限、加超时、加日志、加上下文压缩,让它能稳定跑长任务
  3. 扩展阶段:插件化、配置化、多模型支持、可观测性看板

每个阶段都有明确的验收标准,别跳级。

6.4 那些"看起来不重要"但实际很关键的细节

最后分享几个我在搭 Harness 过程中总结的、文档里基本不会写的小经验:

  • 工具返回结果要带"下一步建议":比如grep没找到结果时,返回"未找到匹配,建议检查关键词或扩大搜索范围",模型会更容易调整策略
  • 循环里加"反思轮":每 N 轮让模型总结一下"目前进展、遇到的问题、下一步计划",能显著减少跑偏
  • 错误信息要"可操作":别只返回"执行失败",要返回"失败原因 + 建议动作",模型才能自我修复
  • 给模型"退出"的勇气:明确告诉它"如果任务无法完成,可以说明原因并停止",否则它会硬撑到轮数耗尽
  • 会话隔离:不同任务的上下文要隔离,别让上一个任务的残留信息污染下一个

这些细节单看都很小,但叠加起来,Agent 的完成率和稳定性会有质的差别。我做过对比,加了这些之后,同一个任务集的端到端成功率从 60% 出头提到了 85% 以上。

搭 Coding Agent Harness 这件事,本质上是在给一个"能力很强但边界感很差"的模型,配一套能约束它、支撑它、观察它的工程系统。ZCode 的 7.3k Star 不是因为它用了多牛的模型,而是因为它把这套系统做得足够扎实、足够开放。你如果准备自己搭,记住一句话:核心循环要简单,工具边界要清晰,上下文要会取舍,安全要能兜底,日志要能回放。这五件事做到位,剩下的就是不断迭代和调优了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进