ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从 VT Code 拆解 Coding-Agent Harness:AI 编程的隐形基础设施

2026/9/7 1:52:03 拓冰建站 浏览量
从 VT Code 拆解 Coding-Agent Harness:AI 编程的隐形基础设施 最近在 Hacker News 上看到一个很有意思的项目VT Code作者给它的定位很简单——My attempt at building a coding-agent harness也就是我尝试构建的一个编码代理 harness。这句话看起来轻描淡写但恰恰点中了当下 AI 编程领域最容易让人困惑的一个空白地带我们每天都在说 Agent、Copilot、AI 程序员但把 LLM 和真实代码仓库连接起来的那一层基础设施到底是什么这篇文章我想从 VT Code 这个项目切入把coding-agent harness这个概念拆开讲清楚。你会明白harness 和 agent 到底是什么关系一个能真正跑代码库任务的 harness内部有哪些核心模块如果自己想实现一个最小可用的 coding harness代码层面大概长什么样什么时候该用现成工具什么时候值得自建以及那些最容易在真实项目里踩坑的地方。如果你最近在关注 Codex、Devin、OpenHands 这类 AI 编码工具或者正打算基于大模型做一版内部编码助手这篇文章值得读完。1. 为什么有了大模型还需要一个 harness很多人对 AI 编程工具有一个误解只要把代码仓库丢给 GPT-4o / Claude / DeepSeek它就能自动把需求变成代码。真实情况远没有这么简单。你给模型一个任务它确实能生成一段代码。但一个真正能胜任编码工程师角色的 Agent需要经历这样一个循环阅读仓库结构理解现有代码定位相关文件和函数生成代码修改方案执行文件编辑运行测试或构建命令根据报错继续修改直到任务完成。这整个循环不是靠一次大模型 API 调用能完成的。它需要一个控制系统去管理模型的行为、上下文、工具操作和环境边界。这个控制系统就是harness。用个不太精准但容易理解的类比大模型像是驾驶技术很强的司机harness 是车里的方向盘、刹车、仪表盘和导航系统。没有 harness模型的能力再强也很难在真实代码库里安全、稳定地完成一次驾驶任务。所以 VT Code 的定位不是又一个代码生成器而是驾驭编码 Agent 的那套基础设施。这个定位在当前 AI 编程工具链里非常关键。2. harness 与 agent 到底有什么区别这是我看很多讨论时觉得概念最混乱的地方。不少人把 harness 和 agent 混为一谈其实这是两个不同层面的东西。Agent是决策和行动的主体。它由 LLM 驱动负责理解意图、规划步骤、生成代码。你把它理解成一个大脑也没有问题。Harness是让 Agent 能安全高效行动的外部系统。它负责管理 Agent 的运行循环loop维护和裁剪上下文提供工具调用接口控制文件读写权限设置沙箱和命令执行边界记录日志和过程数据处理模型返回结果的结构化解析。用一张表格来对比会更清楚对比维度AgentHarness核心职责理解任务、生成决策承载决策、控制执行是否包含大模型是Agent 通常由模型驱动不一定harness 可以对接任意模型是否包含工具会调用工具负责提供和管理工具权限边界本身不关心必须严格约束上下文管理模型侧感知harness 负责组装和裁剪可观测性单次响应全流程可回放更直白地说你可以写出一个聪明的 agent但如果没有一个可靠的 harness这个 agent 只会在你的仓库里胡作非为——乱改文件、执行危险命令、丢失上下文、最后给你留下一堆无法追踪的改动。VT Code 作为 coding-agent harness它的价值重心不在模型多强而在工程侧的控制能力。如果你想深入验证这一点可以去看 OpenAI 放出来的 Codex 相关工作以及社区里对 DeepSeek Harness 安装和配置的讨论。你会发现它们都在解决同一个问题如何让 Agent 在真实环境里跑得又稳又安全。这也正是为何harness 工程最近被越来越多团队当作一个独立的技术方向。3. 一个 coding-agent harness 的核心模块不管你是用现成的 DeepSeek Harness、Codex Harness还是自己搭一套 VT Code 这样的项目成熟的 coding-agent harness 逃不开下面六个模块。3.1 Agent Loop代理主循环这是 harness 的心脏。它决定了一次任务如何被分解成多轮模型调用 工具调用。典型流程是接收用户任务 - 组装初始上下文 - 模型推理 - 判断是否需要工具调用 - 执行工具 - 把结果回填给模型 - 继续推理 - 直到模型给出结束信号没有主循环模型就无法从生成一段话变成完成一个任务。3.2 Context Builder上下文构建器上下文构建器负责把仓库结构、相关文件内容、历史消息、任务描述组装成模型能理解的提示词。它要解决的问题是仓库太大不可能把全部代码塞进上下文信息过旧模型读到的代码可能已经改过了需要控制 token 成本。这个模块做得是否聪明直接决定 Agent 回答质量的上限。上下文构建器的核心不仅在于塞多少内容进去更在于每一步之后保留哪些信息、丢弃哪些信息。如果每一步都把之前的全部对话和历史文件内容塞给模型token 消耗会迅速失控模型对关键信息的注意力也会被稀释。好的做法是做分层管理仓库结构、当前文件内容、历史操作记录、任务目标分别维护在每一步按需组装并对已经过期的结果做及时剪枝。VT Code 这类自研 harness 的价值往往就体现在这种细节设计里。3.3 Tool Registry工具注册表模型不能直接操作真实环境它只能请求调用某个工具由 harness 去执行。常见的工具包括读取文件写入文件执行 shell 命令运行测试搜索代码。工具注册表要做的是定义工具的名称、参数 schema、执行逻辑和返回结构然后把它们暴露给模型。3.4 Permission Boundary权限边界这是 coding harness 最容易被低估、也最容易出事故的部分。模型生成一个rm -rf /并不稀奇真正危险的是 harness 直接执行了它。所以权限模块必须回答Agent 能不能写这个目录Agent 能不能执行网络命令哪类命令需要人工审批在本地自建 harness 场景下最简单的策略是默认只允许操作当前工作区所有高危命令删除、安装依赖、git push默认拦截运行 shell 必须在受限的隔离目录内。3.5 Model Adapter模型适配层好的 harness 不应该绑定某个特定模型。VT Code 以及 DeepSeek Harness 这类工具之所以被社区频繁讨论很大程度上正是因为它们支持通过 OpenAI 兼容接口接入不同的大模型。这样你既可以在本地用开源模型跑通实验也可以通过同一个 harness 切换云端模型做对比。模型适配层要处理API 的请求格式差异流式输出和普通输出的统一工具调用格式的解析错误重试与降级策略。3.6 Observer观测模块没有观测就没有迭代。一个合格的 harness 应该把每一步的模型输入、模型输出、工具调用、耗时、token 消耗全部记录下来。logs目录下每个任务一个子目录是推荐做法方便按任务维度回放。完成的对话可以输出为conversation.json工具执行记录输出为tools.log。这样当任务失败时你可以快速定位是模型决策错误、上下文不足还是工具执行异常而不是对着终端黑屏猜原因。4. 环境准备与前置条件如果你看完上面的模块分析想自己动手搭一个最小 harness不需要依赖 VT Code 本身。因为它的定位是展示如何构建而不是一个开箱即用的成熟产品。你完全可以用通用技术栈实现同构思路。本文示例采用 Python 3.10主要依赖openai作为 OpenAI 兼容接口的客户端 SDK市面上绝大多数模型服务都兼容该协议PyYAML解析配置文件不引入任何重量级框架保持代码可读性。如果你的环境没有安装这些依赖可以执行python -m venv .venv source .venv/bin/activate pip install openai pyyaml注意不同项目的.venv激活命令不同Windows 下是.venv\Scripts\activate。这里不过度纠结版本号搭最小示例时以最新稳定版为准即可。5. 从零实现一个最小 coding harness这一部分是全文的核心。我们不会做一个功能完备的产品而是把 Agent Loop 工具调用 权限控制 上下文管理 这条主干跑通。你把它理解了再看 VT Code、Codex Harness、DeepSeek Harness 的源码就会轻松很多。5.1 配置文件设计一个 harness 最起码要解决两件事模型从哪来Agent 能干什么。我们在项目根目录创建config.yaml# 文件路径config.yaml llm: base_url: https://api.openai.com/v1 api_key_env: OPENAI_API_KEY model: gpt-4o-mini temperature: 0.2 max_tokens: 2048 workspace: root: ./workspace_demo allowed_suffixes: [.py, .md, .txt, .json] max_file_size_kb: 64 harness: default_max_steps: 10 log_dir: ./logs enable_shell_tool: true这里我故意把workspace.root限制在一个单独目录里。生产环境里这个目录应该是一个 git 仓库的隔离副本而不是你的整个磁盘。5.2 工具层实现我们实现三个最基础的工具读文件、写文件、执行命令。# 文件路径tools.py import os import subprocess from pathlib import Path class ToolRegistry: 注册和管理可供 Agent 调用的工具。 def __init__(self, workspace_root: str, allowed_suffixes, max_size_kb): self.workspace Path(workspace_root).resolve() self.allowed_suffixes allowed_suffixes self.max_size_bytes max_size_kb * 1024 self.tools {} self._register_builtin_tools() def _validate_path(self, raw_path: str) - Path: 将相对路径解析到工作区内并阻止逃逸工作区。 p (self.workspace / raw_path).resolve() if not p.is_relative_to(self.workspace): raise PermissionError(f路径 {raw_path} 不在工作区内) return p def _register_builtin_tools(self): self.tools[read_file] { name: read_file, description: 读取工作区文件内容, parameters: {type: object, properties: {path: {type: string}}, required: [path]}, handler: self.read_file, } self.tools[write_file] { name: write_file, description: 写入工作区文件。若文件不存在会创建。, parameters: { type: object, properties: {path: {type: string}, content: {type: string}}, required: [path, content], }, handler: self.write_file, } self.tools[run_command] { name: run_command, description: 在工作区目录下执行 shell 命令。默认超时 10 秒。, parameters: { type: object, properties: {command: {type: string}}, required: [command], }, handler: self.run_command, } def read_file(self, path: str) - str: p self._validate_path(path) if not p.exists(): return f[错误] 文件不存在: {path} if p.stat().st_size self.max_size_bytes: return f[错误] 文件超过 {self.max_size_bytes} 字节限制 return p.read_text(encodingutf-8) def write_file(self, path: str, content: str) - str: p self._validate_path(path) if p.suffix not in self.allowed_suffixes: return f[错误] 后缀名 {p.suffix} 不被允许 p.parent.mkdir(parentsTrue, exist_okTrue) p.write_text(content, encodingutf-8) return f[成功] 已写入 {path} def run_command(self, command: str) - str: if not hasattr(self, _shell_enabled) or not self._shell_enabled: return [错误] shell 工具未启用 try: result subprocess.run( command, shellTrue, cwdself.workspace, capture_outputTrue, textTrue, timeout10, ) output result.stdout[-2000:] if result.stderr: output \n[stderr]\n result.stderr[-1000:] return output or [命令无输出] except subprocess.TimeoutExpired: return [错误] 命令执行超时代码里最值得注意的是_validate_path。它用resolve()把路径转成绝对路径再判断是否在工作区内部。这能防止模型生成../../etc/passwd这种路径时造成越权访问。5.3 Agent 主循环实现接下来是 harness 最核心的部分把模型输出解析成 JSON 工具调用然后执行、回填、再推理。# 文件路径harness.py import json import os import time import traceback from datetime import datetime from pathlib import Path from openai import OpenAI from tools import ToolRegistry SYSTEM_PROMPT 你是一个严谨的编码助手 Agent。 你可以读取文件、写入文件、执行命令来完成用户任务。 工具调用结果必须以 JSON 格式返回 {tool: 工具名, arguments: {参数1: 值1}} 如果不需要调用工具直接输出最终答案。 注意只能操作工作区内文件绝对不要尝试修改工作区之外的内容。 class CodingHarness: def __init__(self, config: dict): self.config config llm_cfg config[llm] self.client OpenAI( api_keyos.getenv(llm_cfg[api_key_env]), base_urlllm_cfg[base_url], ) self.model llm_cfg[model] self.temperature llm_cfg.get(temperature, 0.2) self.max_tokens llm_cfg.get(max_tokens, 2048) ws_cfg config[workspace] self.registry ToolRegistry( ws_cfg[root], ws_cfg[allowed_suffixes], ws_cfg[max_file_size_kb], ) self.registry._shell_enabled config[harness].get(enable_shell_tool, False) self.max_steps config[harness].get(default_max_steps, 10) self.log_dir Path(config[harness][log_dir]) def _call_model(self, messages): 调用大模型并尝试解析工具调用 JSON。 resp self.client.chat.completions.create( modelself.model, messagesmessages, temperatureself.temperature, max_tokensself.max_tokens, ) return resp.choices[0].message.content staticmethod def _parse_tool_call(text: str): 从模型返回文本中解析工具调用 JSON。 start text.find({) end text.rfind(}) if start -1 or end -1: return None try: payload json.loads(text[start:end 1]) if tool not in payload: return None return payload except json.JSONDecodeError: return None def run(self, task: str): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task}, ] log [] for step in range(1, self.max_steps 1): try: reply self._call_model(messages) except Exception as exc: return {error: str(exc), messages: messages, log: log} log.append({step: step, type: model, content: reply}) tool_call self._parse_tool_call(reply) if tool_call is None: messages.append({role: assistant, content: reply}) log.append({step: step, type: final, content: reply}) return {result: reply, messages: messages, log: log} tool_name tool_call[tool] arguments tool_call.get(arguments, {}) log.append({step: step, type: tool_request, tool: tool_name, arguments: arguments}) if tool_name not in self.registry.tools: tool_output f[错误] 未知工具 {tool_name} else: try: tool_output self.registry.tools[tool_name][handler](**arguments) except PermissionError as exc: tool_output f[权限拒绝] {exc} except Exception as exc: tool_output f[工具异常] {exc}\n{traceback.format_exc()} log.append({step: step, type: tool_result, content: tool_output}) messages.append({role: assistant, content: reply}) messages.append({role: tool, name: tool_name, content: tool_output}) return {error: 超过最大执行步数, messages: messages, log: log} def save_log(self, task_id: str, data: dict): self.log_dir.mkdir(parentsTrue, exist_okTrue) log_file self.log_dir / f{task_id}.json data[saved_at] datetime.now().isoformat() log_file.write_text(json.dumps(data, ensure_asciiFalse, indent2), encodingutf-8) return str(log_file)这里有几个设计点需要解释为什么用 JSON 而不是 OpenAI 的 function calling因为 OpenAI 兼容接口里不同平台的 function calling 返回结构不完全一致甚至部分开源模型只认识纯文本 JSON。先用 JSON 文本解析是让最小 harness 具备最大兼容性的务实做法。为什么不把模型返回直接当作命令执行因为在你的 harness 里模型说什么就做什么是最危险的设计。我们把工具调用先解析成结构化数据再经过 ToolRegistry 校验路径、后缀名、shell 开关等于在模型和环境之间加了一道闸门。5.4 入口文件最后写一个入口让任务可以通过命令行触发。# 文件路径main.py import sys import uuid import yaml from harness import CodingHarness def main(): if len(sys.argv) 2: print(用法: python main.py 你的任务描述) sys.exit(1) task sys.argv[1] with open(config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) harness CodingHarness(config) task_id uuid.uuid4().hex[:8] print(f[任务]{task}) print(f[ID]{task_id}) print(f[模型]{harness.model}) result harness.run(task) log_path harness.save_log(task_id, {task: task, result: result}) print(f[日志]{log_path}) if result in result: print(\n 最终输出 ) print(result[result]) else: print(f\n[任务未完成] {result.get(error, 未知错误)}) print(可通过日志文件查看失败原因。) if __name__ __main__: main()6. 运行与结果验证先把示例工作区准备好mkdir -p workspace_demo echo def greet(name): workspace_demo/demo.py echo return fhello {name} workspace_demo/demo.py export OPENAI_API_KEY你的 API Key python main.py 请读取 workspace_demo/demo.py 的内容并新建一个 README.md解释这个函数的作用如果一切正常你会看到类似这样的输出[任务]请读取 workspace_demo/demo.py 的内容并新建一个 README.md解释这个函数的作用 [ID]a1b2c3d4 [模型]gpt-4o-mini [日志]logs/a1b2c3d4.json 最终输出 已经完成。我读取了 demo.py并在 workspace_demo/README.md 中写入了函数说明。然后去检查workspace_demo/README.md是否被正确创建。如何判断 harness 真的工作正常重点不是看最终输出而是看logs/{task_id}.json里的过程数据。打开它确认这些关键结构都存在每一条model类型日志模型是否产生了工具调用请求每一条tool_request日志请求的工具名和参数是否合理每一条tool_result日志工具返回值是否异常最终轮次没有tool_call而是直接输出答案。如果模型生成了 JSON 但工具没有被调用大概率是_parse_tool_call解析失败——先检查模型返回的 JSON 是否符合预期结构再看start和end的截取逻辑有没有漏掉外层花括号。这里要特别提醒config.yaml里的enable_shell_tool我默认设置为false。真正要开启 shell 工具前请务必先在工作区workspace_demo中放一个最小测试文件跑几次验证路径校验没有绕过漏洞再考虑扩大权限。不要在真实生产目录里直接开启 shell。7. coding harness 选型自建还是用现成方案回到 VT Code 本身。它的定位是my attempt这是一个有很强学习性质和个人探索色彩的项目。我自己判断它至少有三层价值第一层学习价值。如果你想理解 coding agent 的底层工作机制读再多的文章都不如自己把 Agent Loop、工具调用、上下文裁剪跑一遍。这类项目是很好的源码教材。第二层定制价值。每个团队对Agent 该干什么、不该干什么的定义完全不同。有的团队希望 Agent 只读代码给出建议有的希望它能自动写单元测试还有的希望它能自己跑构建修构建。通用产品很难同时满足这些需求。自建 harness 最大的优势就是权限边界、工具集合、模型选择都由你说了算。第三层成本价值。像 DeepSeek Harness、Codex Harness 这类工具社区里大量讨论都指向一个方向用户希望用更低的 API 成本完成编码任务。自建 harness 可以精确控制上下文长度和模型档位避免大模型在简单任务上浪费 token。但也要泼一盆冷水。以下情况我建议优先考虑成熟的 harness 方案而不是自建你的目标是快速交付业务功能而不是探索 Agent 架构团队没有专人维护 AI 基础设施你需要深度依赖某个云厂商的生态能力比如私有化部署、审计、细粒度权限策略项目已经进入生产稳定期自建 harness 的维护成本会逐步压过收益。harness 和 agent 的选型不是二选一而是分层搭配。你可以用开源大模型做推理核心用自建或现成的 harness 做执行框架再用任务脚本定义出不同的 agent。这里每层都可以独立替换这正是 modern coding stack 的魅力。8. 常见问题与排查方法问题现象可能原因排查方式解决方案模型返回 JSON 但工具没执行解析逻辑截取范围不对或模型输出包含多余文本打印_parse_tool_call的解析结果和原始 reply调整 JSON 提取逻辑改用正则或独立 JSON 解析函数工具调用报路径不在工作区内模型拼接了绝对路径或..跳出了工作区查看tool_request日志里的参数检查_validate_path的 resolve 结果不要盲目放行超大文件读取被拒max_file_size_kb限制触发查看tool_result返回的错误信息提高限制或引导模型先看文件头部前 N 行模型频繁尝试调用不存在的工具工具名与模型预期不一致或系统提示不够明确检查 SYSTEM_PROMPT 是否列出全部工具把工具名和参数说明写进 system promptshell 命令被拦截或无法执行enable_shell_tool未开启或命令执行超时查看 config 配置和run_command超时日志按需开启 shell检查命令是否合理上下文在几步后就开始混乱历史消息过长模型遗忘关键任务目标查看日志中 messages 数量加入消息裁剪机制定期压缩历史API 超时或限流并发过高或模型服务不稳定查看客户端错误信息和响应时长增加重试和退避逻辑切换低价备用模型9. coding harness 的工程实践建议如果你真的打算在公司里落地一套 coding-agent harness而不仅是个人实验下面这些实践建议值得直接收藏。9.1 默认最小权限我在代码里反复强调路径校验不是因为模型有多坏而是因为 LLM 生成的工具调用具有不确定性。你无法预判它在极端情况下会拼接出什么样的命令。所以 harness 的默认原则应该是只读能力最优先开放写操作限制在白名单目录高危命令默认拒绝需要人工确认的操作明确返回需要审批。9.2 所有执行都要留痕harness 的日志远比普通应用日志重要。它能帮你回答三个灵魂问题这个改动是谁让改的模型当时看到了什么工具执行实际发生了什么建议在日志里记录每一步的原始输入输出并附带 token 消耗和执行耗时。VT Code 这类项目的源码里日志和回放设计也应该是重点阅读对象。9.3 模型适配层要早点抽出来不要把所有逻辑写在某一个模型的 SDK 上。只要 API 是 OpenAI 兼容协议就可以通过base_url动态切换模型。这样当团队想从商业模型切到开源模型或从云端模型切到本地模型时harness 主逻辑完全不用改动。9.4 先跑通测试集再做功能迭代coding harness 复杂度不算低。建议你准备一个固定的小任务集例如读取指定文件并总结修复一个已知 bug新建一个单测文件并运行递归列出仓库结构并说明项目用途。每次修改 harness 代码后先跑这个测试集确认没有引入回归。一个变更如果让简单任务的成功率都下降就不要带上生产环境。9.5 永远准备一个手工模式再完善的 harness 也可能在真实任务里犯错。建议给系统加一个逃生舱当 Agent 连续多次失败或工具调用明显不合理时把控制权交还给人类。这个机制听起来简单但在实际项目里比任何强化学习都管用。10. 从 VT Code 出发下一步可以研究什么VT Code 是个小而精确的起点。如果你把它读懂了会发现 coding-agent harness 的后续方向其实非常清楚上下文工程如何基于代码仓库切片动态构建高质量 prompt工具设计的边界什么时候该把功能做成工具什么时候该直接让模型生成代码多 Agent 编排harness 之上如何协调阅读者 Agent、编码 Agent和验证 Agent评估体系怎么衡量一个 harness 是变好了还是变坏了不能只看一两个任务的结果。我个人观点是未来一段时间内模型能力的差距会逐步缩小真正的分化会发生在 harness 这一层。谁能把 Agent 控制得稳、可控、成本低谁就能在实际项目中胜出。如果你想沿着这个方向继续下一步的实践路径通常是这样几个递进层次先复刻本文的最小 harness 跑通流程再阅读 VT Code、DeepSeek Harness 或 Codex Harness 等项目的源码理解它们的主循环和工具注册表设计然后尝试给最小 harness 增加消息裁剪、人工审批、模型回退这些能力。每加一个模块你对Agent 如何才能真正干活的理解都会更深入一层。最后给你留一个动手任务把本文第 5 节的示例代码跑通之后尝试让 harness 完成这样一个多步骤任务——读取当前工作区所有 Python 文件统计每个文件的函数数量并把统计结果写入 SUMMARY.md。你会发现当 Agent 需要多轮工具调用时上下文管理和工具结果的稳定性才是真正决定任务成败的关键。这也正是 harness 存在的价值。