coding-agent harness 从零搭建:工具、权限与 agent loop 实战

发布时间:2026/9/7 21:48:03
coding-agent harness 从零搭建:工具、权限与 agent loop 实战 VT Code 这个项目标题里最值得拆的词不是 Code而是 harness。最近关于 deepseek harness、codex harness 这类主题的讨论明显多起来很多人已经不再满足于在聊天窗口里让模型写代码而是想把模型接进真实的仓库目录、命令行和 CI 流程里让它真的去改文件、跑测试、看报错、再迭代修复。这些事单靠模型本身做不完需要一层连接模型与工程环境的基础设施这层结构就是 coding-agent harness。如果你也在考虑自己搭一个 coding-agent harness或者正在纠结直接上现成框架还是自己写一套这篇内容可以当一份从设计到落地的参考。它不会替你选型但会把边界条件、搭建步骤、关键参数和排查顺序讲清楚。VT Code 这类从零尝试的项目最值得看的通常不是功能数量而是作者能不能把 agent loop、工具调用、权限控制和日志追踪这几块组织明白。下面按实际落地顺序拆。1. coding-agent harness 到底解决什么问题1.1 先分清 agent 和 harness第一次接触这个概念的人通常把 agent 和 harness 当成同一个东西实际上分工完全不同。agent 指的是模型本身以及围绕模型建立的决策循环。它会根据当前任务判断下一步做什么读文件、改代码还是运行测试然后根据观察结果继续推理。可以把它理解成一个会思考的执行者负责“接下来做什么”。harness 是包裹在 agent 外层的工程结构。它负责提供工具、管理权限、维护上下文、记录日志、处理重试和队列。agent 说“我想运行 python 脚本”harness 负责决定能不能跑、在哪里跑、超时多久、结果怎么返回、出错怎么办。简单说agent 负责决策harness 负责让决策安全、可控、可追踪地落地。最近很多人在搜 deepseek harness 怎么安装、codex harness 怎么配置本质上都遇到了同一个问题模型 API 已经能输出代码和工具调用但要把这些调用真正映射到文件系统和终端还需要自己解决一层结构问题。也有人把 harness 理解成 IDE 插件这不太准确。插件只是入口harness 是入口背后处理任务分发的整套机制。1.2 为什么不能直接把模型输出接进终端最容易踩的坑是一开始图省事把模型返回的代码片段直接拼到 shell 命令里执行。单条简单任务可能看不出问题任务稍微复杂立刻暴露四件事。第一没有权限边界。模型一旦接收到带恶意的输入或生成了意外的清理命令可能执行任意操作。第二没有上下文管理。改完一个大文件后把整个内容回传两三个回合就能把 token 预算打满。第三没有可观测性。模型改了哪个文件、执行了什么命令、中途发生了什么完全没有记录。第四没有失败恢复。命令执行到一半挂了是重试还是继续没有状态就只能人工介入。harness 的核心价值就是把以上四件事变成可配置、可控制、可审计的流程。这也是我建议不要跳过 harness 直接裸接模型 API 的原因。2. 动手前先定边界工具、权限和沙箱2.1 工具集先小后大在 coding-agent harness 里工具是模型可以调用的函数常见的有 list_files、read_file、write_file、run_command、search_code、git diff 等。新手最容易犯的错是第一天就把十几个工具全部注册进去。工具越多模型越容易选错你的排查范围也越大。我一般建议从四个工具开始list_files、read_file、write_file、run_command。这四个已经能覆盖“看目录、读文件、改文件、跑测试”的最基本闭环。先把闭环跑通再加搜索工具、git 工具、网络请求工具。先小后大的原因很实际每个工具都意味着新的入参格式、错误分支和权限风险。工具一旦被模型学会使用后面想摘掉往往要重新调 prompt。保持最小集agent loop 的调试范围会小很多。2.2 权限边界和沙箱策略文件读写和命令执行是 harness 里风险最高的两个位置。文件读写要限制在工作目录内防止模型写到系统目录命令执行要过白名单比如只允许 python、pytest、node、npm、git 这类常用命令并且给每条命令设置超时。白名单之外的操作先标记为需要人工审批。一个常见的配置结构是这样的# harness 权限配置示例实际参数按你的环境调整 sandbox: workdir: ./workspace allowed_commands: [python, pytest, node, git, ls] deny_commands: [rm -rf, chmod, sudo] command_timeout_seconds: 30 allow_network: false max_output_chars: 8000allow_network 默认设成 false是不少人踩过坑之后的共识。命令执行阶段如果有网络权限可能会出现下载依赖、访问外部服务等难以预测的行为。纯代码任务没有联网需求先关掉更稳。低配置环境也不用担心harness 本身不依赖 GPU。真正消耗资源的是模型 API 调用和命令执行所以只要你的机器能跑代码和测试就能把这个最小沙箱跑起来。注意deny_commands 里的规则要按实际环境调整这里的示例只用于说明结构不要直接照搬。2.3 任务类型决定架构harness 的架构和工作流要被要处理的任务类型反过来定义。“修一个 issue”“给整个仓库补注释”“在多个仓库里批量替换公共依赖”这三类任务对 harness 的要求完全不同。修 issue单任务、单沙箱、上下文小一个 agent loop 就能搞定。批量重构需要任务队列、文件级隔离、结果汇总和失败重试。长期服务需要 API 接口、异步任务、持久化状态和回调通知。我搭第一个版本时只考虑单任务等单任务稳定了才加队列。这是最省时间的路线。不要一上来就设计分布式调度很多 harness 项目死在没必要的复杂度上。3. 最小可运行 harness 的搭建顺序3.1 环境和依赖搭建最小 harness 需要的环境不复杂。以 Python 为例3.10 及以上通常就够了核心依赖是 openai SDK因为目前大多数模型厂商都提供 OpenAI 兼容接口DeepSeek 也支持这套协议。只要在客户端里设置 base_url 和 api_key就能在不同模型之间切换。还需要一个干净的代码目录、一个终端以及能跑通的基本命令环境。有人会问要不要先做个桌面端早期完全没必要。命令行加日志就是最快的验证方式等流程稳定了再考虑界面化。第一阶段不要装太多东西。OpenAI SDK 之外再加一个日志库和 YAML 读取库就够了。我见过有人第一天就把 Redis、Celery、Docker 全装上结果问题都分不清是模型问题还是基础设施问题。API key 用环境变量注入不要写进代码或配置文件。模型名称、base_url 这类可以放配置里方便切换。3.2 模型适配层模型适配层的作用是把不同提供商的接口差异挡在 harness 外部。你在模型层只面对一个统一的客户端换模型时只改配置不改 agent loop。配置示例# 模型配置示例base_url 和 model 按实际服务商填写 model: provider: deepseek base_url: https://api.deepseek.com model: deepseek-chat api_key_env: DEEPSEEK_API_KEY temperature: 0.2 max_tokens: 8192这段是示范配置。实际落地时先确认你的模型服务商提供的 base_url 和模型名。像 deepseek-chat、deepseek-reasoner 这类不同模型上下文策略和 token 消耗也不一样reasoner 类模型思考链长做 harness 时要单独评估。适配层通常只做两件事把 harness 的消息列表转成厂商需要的格式把厂商返回的工具调用转成统一的 tool_call 结构。很多框架已经封装好了但自己写一遍你会更清楚工具调用哪里容易出错。3.3 工具注册与执行工具层是 harness 与真实环境之间的桥梁。每个工具包含三部分名称、参数 schema、执行函数。参数 schema 用来在请求里告诉模型这个工具接受什么参数执行函数负责真正做事。极简工具注册结构如下# 极简工具注册示例不是完整实现 TOOL_REGISTRY { list_files: { schema: {type: object, properties: {path: {type: string}}}, fn: list_files_impl, }, read_file: { schema: { type: object, properties: { path: {type: string}, max_chars: {type: integer, default: 4000}, }, required: [path], }, fn: read_file_impl, }, write_file: { schema: { type: object, properties: {path: {type: string}, content: {type: string}}, required: [path, content], }, fn: write_file_impl, }, run_command: { schema: { type: object, properties: {command: {type: string}}, required: [command], }, fn: run_command_impl, }, }写工具执行函数时有三点要特别留意。路径必须做规范化处理防止路径穿越到工作目录之外。命令必须经过白名单过滤和超时控制。返回给模型的输出要做截断避免工具结果把上下文撑爆。read_file 加上 max_charsrun_command 加上输出截断都是常见做法。3.4 agent loop 与单任务验证agent loop 是 harness 的核心循环。流程很直观把系统提示和用户任务发给模型模型返回文本或工具调用如果有工具调用harness 执行对应工具把结果作为新消息追加回去重复这个过程直到模型不再调用工具或达到最大步数。极简 loop 骨架# 极简 agent loop仅用于理解结构 def run_agent(task, registry, client, system_prompt, max_steps20): messages [ {role: system, content: system_prompt}, {role: user, content: task}, ] for step in range(max_steps): response client.chat.completions.create( modelMODEL, messagesmessages, toolsbuild_tools_schema(registry) ) message response.choices[0].message messages.append(message) if not message.tool_calls: return message.content for call in message.tool_calls: tool registry.get(call.function.name) if tool is None: result f未知工具: {call.function.name} else: result tool[fn](**json.loads(call.function.arguments)) messages.append( { role: tool, tool_call_id: call.id, content: str(result), } ) return 达到最大步数任务未完成这段代码省略了错误处理和上下文压缩但结构是对的。第一次验证时用一个最简单任务测试“读取当前目录的 test.py运行它如果报错就修复”。如果能跑通说明 harness 的最基本链路已经成立。这一步的成功标准有三条模型能正确发起工具调用工具结果能正确回传模型能在拿到结果后继续推理并输出修复后的代码。三条都满足再往下一步加功能。想读源码的人优先看 agent loop 和工具执行器这两个文件其他都可以往后放。4. 核心参数和判断标准4.1 并发、超时、重试单任务跑通之后紧接着要面对的是参数调优。先关注三组参数并发数、超时时间、重试次数。它们直接决定稳定性。参数起始建议判断标准注意事项并发数1任务成功率、API 限流、磁盘 IO多任务同时改同名文件会冲突工具超时30 秒按命令调整命令是否卡住、是否在等待输入测试套件通常需要更长整体任务超时按 max_steps 估算agent 是否无限循环依赖工具白名单和最大步数重试次数2 次失败是否幂等写入类操作不能盲目重试并发数默认从 1 开始确认稳定后再往上加。并发上去之后要留意 API 限流、本地磁盘 IO 和命令执行冲突。两个任务同时改同一个文件就会互相覆盖这不是并发参数能解决的需要任务隔离或文件锁。注意这里不要一上来就开最大并发先用一条样例确认输入、输出和日志都正常。超时分两层工具执行超时和整体任务超时。工具超时防止命令卡死占住沙箱整体超时防止 agent loop 在一个任务上无限循环。run_command 的默认超时我一般给 30 到 60 秒具体看命令类型。重试策略要看操作是否幂等。读文件和只读命令可以放心重试写文件和部署操作重试要谨慎第二次执行可能产生叠加副作用。简单规则只在 API 调用失败这类临时错误上自动重试业务逻辑错误一律记录后人工判断。4.2 token 预算和上下文管理上下文长度是 coding-agent harness 最容易翻车的指标。一个仓库动辄几千个文件每次工具调用都会消耗 token。如果工具不做输出截断三个回合就能把上下文撑爆。要提前规划三件事每次模型请求的 max_tokens 上限、工具输出的单次截断长度、回传文件内容的上限。我一般把单个工具输出截断在 4000 到 8000 字符超过部分提示模型“内容过长已被截断请使用更精确的读取方式”。上下文接近上限时有两种常见策略把早期步骤的对话压缩成一两句摘要或者把 tool 消息里的大段内容移除。前者保留推理线索适合复杂任务后者实现简单适合长任务的早期阶段。起步阶段先用截断控制规模等日志能明确看到 token 消耗后再设计压缩策略。判断上下文是不是瓶颈可以看一个现象模型开始原样复述工具结果里的内容而不是基于内容做判断。一旦出现说明前面的上下文已经干扰了它的注意力。4.3 日志、trace 与成本没有日志的 harness 没法调试。模型行为有随机性同样的任务两次执行可能走完全不同的路径。我建议从第一天就记录结构化日志每个步骤至少包含步骤编号、模型名称、调用工具、工具参数、工具结果摘要、耗时、token 消耗。这些信息既用来排查也用来估算成本。到多任务和多人使用阶段trace 比日志更重要。每个任务分配一个 run_id所有日志、工具调用、模型响应都挂在 run_id 下面。排查问题时可以直接拉出某个任务从开始到结束的完整时间线。成本要按 run_id 汇总 token 消耗。不同模型价格差异很大reasoner 类模型思考链长token 消耗比普通对话模型高不少。不做量化和上限控制批量任务跑一晚账单可能超出预算。我一般会在配置里加一个单任务 token 上限超出直接终止并标记。5. 从单任务到批量任务5.1 先跑通一条再开队列harness 最有价值的场景是批量一次给几十个 issue、几十个文件、几十次重构请求。但批量任务和单任务是两种复杂度。我的路径是单任务跑通循环跑三条类似任务再加队列。直接上队列的典型症状第一条任务成功第二条开始出现输出目录冲突、同名文件覆盖、任务间上下文污染。这些问题跟模型能力无关纯粹是工程层没准备好。# 队列配置示例 queue: concurrency: 2 retry_max: 2 task_timeout_seconds: 600 output_dir: ./results metadata_file: metadata.json队列阶段最需要盯的是失败任务。单任务失败可以人工处理批量失败如果没有记录十个小时白跑。所以每条任务结束都要写状态success、failed、timeout、skipped并保留失败原因。批量任务最怕的不是失败而是失败没有任何记录。5.2 输出命名和结果一致性批量任务里最常见的返工原因是输出没有固定命名规则。修复任务的补丁、生成的测试、重构后的文件如果不带 run_id跑完后根本对不上号。一个我常用的约定每次任务创建独立输出目录目录名包含 run_id 和时间戳所有产物写到这个目录最后生成 metadata.json记录任务描述、输入参数、模型型号、开始结束时间、token 消耗和最终状态。这样不管任务成功还是失败都有据可查。{ run_id: 20250604-001, task: 修复 test.py 中的路径拼接错误, model: deepseek-chat, status: success, started_at: 2025-06-04T10:00:00Z, finished_at: 2025-06-04T10:03:12Z, tokens: 15234, artifacts: [workspace/20250604-001/fix.patch] }这个 metadata.json 不仅方便人看也方便程序汇总。批量任务跑完直接遍历所有 metadata 文件就能生成统计报表成功率多少、平均耗时多少、哪里最容易失败。5.3 接口化和 CI 接入当任务可以由程序发起时harness 就不再只是脚本而是服务。最简单的方式是加一层 HTTP 接口提交任务、查询状态、获取结果。接口提交任务时请求里要包含任务内容、可用的工具范围、可选的模型参数。返回一个任务 ID客户端轮询状态或等服务端通过 webhook 回调通知。不要每次请求都新建一个 agent 进程而是用任务队列把请求排队harness 进程按顺序消费。极简接口示例# 极简任务提交接口示例省略鉴权与完整字段 app.post(/tasks) def create_task(payload: dict): task_id queue.submit(payload) return {task_id: task_id, status: queued} app.get(/tasks/{task_id}) def get_task(task_id: str): return queue.query(task_id)接入 CI 是另一个常见场景比如 push 之后让 agent 根据 PR 标题生成变更说明。这类任务对稳定性和超时要求更高因为 CI 任务通常有时间上限。我建议先做超短任务比如单文件改动再逐步扩展到多文件。CI 里的执行环境往往比本地更干净但也更受限需要提前确认工作目录、依赖安装和网络策略。6. 常见坑和排查顺序6.1 先把失败现象分类失败现象先分类再排查效率会高很多。coding-agent harness 常见现象大概有这几类。模型不调用工具只输出文本多半是 prompt 或工具定义问题模型不知道工具存在或不知道什么时候该用。模型反复调用同一个工具原地打转可能是上下文里缺少足够的观察结果或者 max_steps 设置太高。工具执行报错但模型没感知工具返回的错误信息被截断模型看不到关键错误行。整体任务卡住超时先查是不是有命令在等待输入比如 git 在交互式询问这在批量运行时经常发生。输出质量波动大结合 token 消耗和上下文长度判断很多时候是上下文被早前步骤污染了。6.2 固定顺序排查不要上来就改代码排查时我按固定顺序来不会一上来就改 harness 代码。第一步看输入任务描述是否清晰有没有明显歧义输入文件编码、路径、大小是否正常。第二步看模型响应把模型每一步返回的原始内容打到日志里看它在想什么是没看懂任务还是不会用工具。第三步看工具结果命令退出码、stdout、stderr 是否完整回传路径拼接是否正确。第四步看环境依赖版本、API key、网络、磁盘空间、权限。第五步才看 harness 代码本身。几个常见判断模型没说调用工具先查 tools schema 是否真的传给了 API。工具返回空先查执行函数的工作目录和相对路径。命令超时先查是否在等待输入。结果质量差先查上下文是否过长。成本异常高先查是否陷入工具循环。这个顺序看起来很基础但能解决大部分问题。很多时候根本不是模型回答得差而是工具输出被截断、路径拼接错误、或者环境变量没注入。6.3 不是所有场景都需要自己造 harness最后说点实在的不是所有场景都值得自己造 harness。如果你只是想在自己电脑上让 DeepSeek、Codex 这类模型帮忙改几个文件现成的工具链已经够用没必要重新写一套。如果你要处理的是内部业务流程、需要深度定制的权限模型、或者想把 agent 结果与自己的数据系统打通自建 harness 才是有回报的选择。VT Code 这类项目的价值不在于让你直接照抄代码而在于展示和验证一种组织方式。自己搭 harness 最大的收获是你会真正理解模型调用、工具执行、上下文管理和任务调度这些环节是怎么互相咬合的。第一次做不一定要追求功能全但一定要把日志、权限和任务边界想清楚。踩过几次坑之后会发现很多问题不是模型能力不够而是 harness 这一层把输入和过程搞乱了。先跑稳单任务再谈批量、接口和 CI是比堆功能更靠谱的路线。