Harness Engineering 从零理解到动手实践:用 AGENTS.md 与状态机搭一套可验证的 AI Agent 反馈回路
1. 为什么你的 AI Agent 总是“嘴上说做完了”如果你正在用 Claude Code、Cursor 或者自己写的 Agent 跑多步骤任务大概率遇到过这几个场景Agent 说“已完成登录模块修复”你打开文件一看只改了个注释长任务跑到一半它开始重复调用同一个失败命令你让它自己检查代码它永远回复“看起来没问题”。这些问题的根因往往不在模型本身而在于缺少一套外部的运行控制系统。Harness Engineering 就是解决这个问题的工程方法——它不优化模型参数而是给 Agent 搭建“缰绳 马鞍 跑道护栏 反馈镜子”。本文会从零带你搭一套最小可用的 Agent 工程用 AGENTS.md 定义行为边界用状态机锁定任务流转用反馈回路做结果校验最后跑一次端到端验证。适合谁看正在做 AI coding 工具链的后端工程师、想让 Agent 稳定跑长任务的团队、以及被“提前宣布胜利”折磨过的开发者。读完你能拿到一份可复制的 AGENTS.md 骨架、一段状态机配置代码以及一个能立刻跑通的验证动作。2. 前置准备TaoToken 统一 Key 与运行环境在动手之前先把模型调用通道准备好。我试过在多个项目里分别维护不同厂商的 Key切换模型时改配置改到崩溃。TaoToken 在这里的作用是提供统一的 Key/API 通道让你在 Agent 运行层只配置一次后续换模型不用动业务代码。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址https://taotoken.net/api你需要准备的东西不多一个可用的 API Key在控制台创建见下方 deep linkNode.js 18 或 Python 3.10 运行环境一个测试用的代码仓库本文用 Next.js TypeScript 项目举例其他技术栈同理创建 Key 的路径https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite如果你用的是 Claude Code 这类编码 Agent接入文档在这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意Key 只放在环境变量里不要写进 AGENTS.md 或任何会被 Agent 读取的文件。后面权限边界那一节会专门讲怎么用代码拦住 Agent 碰密钥文件。3. 可复制配置AGENTS.md 骨架 状态机 反馈回路3.1 AGENTS.md 骨架让 Agent 读得懂你的项目AGENTS.md 的本质是给 Agent 看的项目说明书。README 给人看AGENTS.md 给 AI 看。关键原则是渐进披露——不要把全部文档塞进去只保留最关键的三类信息WHAT项目是什么、HOW怎么跑、RULES什么不能碰。在项目根目录创建AGENTS.md# AGENTS.md ## 项目概览 Next.js 14 TypeScript 全栈项目使用 App Router。 ## 技术栈 - 框架Next.js 14App Router禁止 Pages Router - 语言TypeScript 严格模式禁止 any - 样式Tailwind CSS禁止 CSS Modules - 数据库Prisma PostgreSQL - 测试Vitest Testing Library ## 开发命令 - 安装依赖pnpm install - 开发服务器pnpm dev - 运行测试pnpm test - 类型检查pnpm typecheck - 代码检查pnpm lint ## 架构约束 - API 路由放在 app/api/ 下 - 业务逻辑放在 lib/ 下 - 环境变量通过 env.ts 统一管理禁止硬编码 - 禁止修改 .env、secrets/、config/production/、.git/ ## 验证方式 改完代码后必须依次执行 1. pnpm typecheck 2. pnpm lint 3. pnpm test 三项全部通过才算完成禁止跳过。如果是 monorepo可以在子包目录下再放一份packages/web/AGENTS.mdAgent 在不同目录下读取到的规则更精准。3.2 状态机配置把任务流转锁死在轨道里软约束写在 Prompt 里的“请先做计划”不可靠模型会忘、会跳过。硬约束要写进执行层。下面是一个四阶段状态机用 Python 实现你可以直接复制到自己的 Agent 运行层from enum import Enum class AgentPhase(Enum): RESEARCH research PLAN plan EXECUTE execute VERIFY verify class PhaseStateMachine: ALLOWED_TRANSITIONS { AgentPhase.RESEARCH: [AgentPhase.PLAN], AgentPhase.PLAN: [AgentPhase.EXECUTE, AgentPhase.RESEARCH], AgentPhase.EXECUTE: [AgentPhase.VERIFY], AgentPhase.VERIFY: [AgentPhase.EXECUTE, AgentPhase.PLAN], } PHASE_PERMISSIONS { AgentPhase.RESEARCH: [read_file, search_code, list_files], AgentPhase.PLAN: [read_file, create_plan], AgentPhase.EXECUTE: [read_file, write_file, run_command], AgentPhase.VERIFY: [run_tests, run_lint, run_typecheck], } def __init__(self): self.current AgentPhase.RESEARCH def can_transition(self, target: AgentPhase) - bool: return target in self.ALLOWED_TRANSITIONS[self.current] def can_execute(self, action: str) - bool: return action in self.PHASE_PERMISSIONS[self.current] def transition(self, target: AgentPhase): if not self.can_transition(target): raise PermissionError( f非法状态迁移{self.current.value} - {target.value} ) self.current target这段代码的核心价值Agent 在 RESEARCH 阶段想直接改代码会被can_execute(write_file)拦下想从 RESEARCH 跳到 EXECUTE会被can_transition拒绝。它必须老老实实先出计划。3.3 反馈回路让 Agent 犯错后越来越稳反馈回路分三层从低成本到高成本依次叠加。第一层是自动化验证改完代码强制跑 typecheck lint testclass FeedbackLoop: def run_verification(self, changed_files: list[str]): results [] for cmd in [pnpm typecheck, pnpm lint, pnpm test]: r self.run_command(cmd) results.append({ check: cmd, passed: r.success, output: r.stdout[-500:], errors: r.stderr[-500:] if not r.success else None, }) return { passed: all(r[passed] for r in results), details: results, }第二层是执行与评审分离。让一个 Agent 写代码另一个独立会话按标准审查避免“自己写自己夸”async def dual_agent_review(task, executor, reviewer): MAX_ROUNDS 3 for _ in range(MAX_ROUNDS): result await executor.execute(task) review await reviewer.review( task_descriptiontask.description, code_diffresult.diff, review_criteria[ 是否完成所有要求, 是否有 bug, 是否遵循 AGENTS.md 规范, 是否存在安全隐患, ], ) if review.is_good_enough(): return result task task.with_feedback(review.comments) return {status: escalate_to_human, result: result, review: review}第三层是错误经验持久化。维护一份.harness/lessons-learned.md每次 Agent 犯错后归因并更新约束# .harness/lessons-learned.md ## 2026-03-20: Prisma 迁移必须在测试前执行 - 问题修改 schema 后没跑 migrate测试全挂 - 修复在验证步骤中加入 migrate 检查 - 状态已纳入硬约束 ## 2026-03-18: 不要在 middleware 中直接 throw - 问题导致页面白屏 - 修复补充框架约束说明 - 状态已写入 AGENTS.md3.4 权限边界高风险目录靠代码拦不要把“别碰生产配置”只写在提示词里。下面这段代码直接拦住 Agent 对敏感路径的访问class PermissionBoundary: FORBIDDEN_PATHS [.env, secrets/, config/production/, .git/] def check_file_access(self, file_path: str, operation: str): for forbidden in self.FORBIDDEN_PATHS: if file_path.startswith(forbidden): raise PermissionError( f禁止{operation}文件{file_path} )4. 验证请求跑一次端到端 Agent 任务配置写完了现在跑一次完整流程验证。假设任务是“修复src/login.tsx中的登录 bug”。4.1 启动会话并加载 AGENTS.mdAgent 启动时Harness 读取根目录 AGENTS.md初始化状态机为 RESEARCH 阶段。此时 Agent 只能调用read_file、search_code、list_files。4.2 观察状态迁移与拦截Agent 请求读取login.tsxHarness 检查 RESEARCH 阶段允许read_file放行。接着 Agent 想直接改代码Harness 检查write_file不在 RESEARCH 权限列表拒绝。Agent 转而请求进入 PLAN 阶段状态机校验RESEARCH - PLAN合法允许。4.3 执行与自动验证Agent 在 PLAN 阶段生成修复计划请求进入 EXECUTE。Harness 允许PLAN - EXECUTEAgent 修改login.tsx。文件修改事件触发验证守卫Harness 强制进入 VERIFY 阶段自动执行pnpm typecheck pnpm lint pnpm test如果三项全部通过任务标记完成。如果测试失败错误输出被反馈给 Agent状态回退到 EXECUTE 重试。4.4 成功结果长什么样一次成功的端到端运行你会看到类似这样的日志[Harness] Phase: RESEARCH - PLAN (allowed) [Harness] Phase: PLAN - EXECUTE (allowed) [Harness] File write detected: src/login.tsx [Harness] Auto-trigger verification guard [Harness] Phase: EXECUTE - VERIFY (forced) [Harness] typecheck: PASS [Harness] lint: PASS [Harness] test: PASS (12 passed, 0 failed) [Harness] Task completed with verification关键点Agent 不一定知道状态机代码长什么样但它会真实感受到哪些动作被允许、哪些被拦下、什么才算完成。5. 本篇常见错排查5.1 Agent 不读 AGENTS.md检查文件是否在项目根目录文件名大小写是否精确匹配。部分工具需要显式配置读取路径确认你的 Agent 运行层有没有加载逻辑。如果用的是 Claude Code接入配置参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite5.2 状态机迁移报 PermissionError先打印self.current和target确认迁移方向在ALLOWED_TRANSITIONS里。常见错误是 VERIFY 失败后想直接回 RESEARCH但合法路径是VERIFY - EXECUTE或VERIFY - PLAN。5.3 验证命令全部通过但 Agent 仍说没完成检查 Agent 的输出解析逻辑。有些模型会把pnpm test的输出误判为失败因为 stderr 里有 warning。建议在 FeedbackLoop 里只以 exit code 为准不要用文本匹配判断成功。5.4 长任务跑到后面上下文爆炸不要让一个会话死扛到底。设置上下文利用率阈值比如 60%超过就生成 handoff 文档开新会话继续class ContextManager: MAX_CONTEXT_UTILIZATION 0.6 async def run_with_reset(self, agent, task): subtasks self.decompose_task(task) for subtask in subtasks: if agent.context_utilization self.MAX_CONTEXT_UTILIZATION: handoff await agent.generate_handoff( prompt总结1.已完成 2.当前进度 3.下一步 4.注意事项 ) agent agent.fresh_session() agent.load_context(handoff) await agent.execute(subtask)5.5 循环失败检测没生效确认LoopDetector的record_failure在每次工具调用失败后都被调用。常见遗漏是只在异常捕获里记录但 Agent 返回successFalse时没记录。建议在工具调用统一出口处埋点。6. 下一步把反馈回路接进你的编码工作流最小可用版本跑通后下一步是把它接进日常编码。如果你主要用 Claude Code 做长期编码任务可以把状态机和验证守卫配置成 Coding Plan 的一部分让每次代码修改都自动走一遍验证回路https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite想先验证模型对话和工具调用是否正常可以从模型对话入口测试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档和 API Key 管理分别在这里接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keyshttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite最后留一个我踩过的坑AGENTS.md 不要一次写太长。我第一版写了 300 多行结果 Agent 读取后反而忽略了关键约束。后来砍到 60 行以内只保留技术栈、命令、禁区、验证方式四块遵守率明显提升。渐进披露比全量灌输有效得多。