☰
Harness Engineering 实践指南:用 AGENTS.md 与 ESLint 为 Claude Code 搭建可复现的工程约束
2026/9/28 18:11:07 网站建设 项目流程

1. 为什么 Claude Code 在真实项目里总是“跑偏”

Claude Code 这类 AI 编程代理在 demo 里表现惊艳,一旦放进真实仓库就开始出问题:改完 A 文件忘了同步 B 文件的类型定义,随手import axios绕过项目封装的请求层,提交前不跑 lint,跨会话切换后完全不知道上一轮做到哪。这些不是模型能力问题,而是 Harness Engineering 要解决的问题——Harness = Agent - Model,也就是代理中除模型以外的一切:约束、工具、文档、反馈循环。

我试过在一个中型 Vue + TypeScript 项目里让 Claude Code 连续工作三天,第一天的产出质量最高,第二天开始出现重复造轮子,第三天直接在types/里 import 了stores/,把架构分层彻底打穿。后来把 AGENTS.md、CLAUDE.md 和 ESLint 架构规则补齐,配合 TaoToken 统一 Key 通道,同样三天的工作量,返工率下降了大半。这篇就把这套可复现的工程约束骨架拆开讲清楚,适合正在把 Claude Code 接入真实项目的开发者,也适合想让 AI 代理长期稳定干活的团队。

核心思路一句话:能用机制检查的,就不要只写在文档里让 Agent“记住”。文档负责告知,ESLint 和脚本负责强制,Hook 负责在错误发生的毫秒级给出纠正信号。

2. 前置准备:用 TaoToken 统一 Key 与 API 通道

在写任何约束文件之前,先把模型接入通道固定下来。Claude Code 默认走 Anthropic 官方端点,但团队协作时经常遇到 Key 分散、额度难管、切换模型要改配置的问题。TaoToken 提供统一的 Key 和 API 通道,把模型对话、Coding Plan、API Keys 管理收敛到一个入口,Claude Code 只需要改一个环境变量就能接上。

官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台生成 Key。API 基地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接填进配置即可。

Claude Code 的接入方式是在项目根目录或全局环境里设置两个变量。macOS / Linux 下写入 shell 配置:

export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-你的TaoToken密钥"

Windows PowerShell 用:

$env:ANTHROPIC_BASE_URL = "https://taotoken.net/api" $env:ANTHROPIC_API_KEY = "sk-你的TaoToken密钥"

如果你用的是 Claude Code 的 settings 文件,也可以写进.claude/settings.json的env字段,这样团队成员拉下仓库后只需要填自己的 Key,端点不用改。Key 的生成和管理在控制台的 API Keys 页面:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= 。

注意:Key 不要提交进 git。把.env和.claude/settings.local.json加进.gitignore,仓库里只保留.env.example模板。

接入完成后,先用一次模型对话验证通道是否通:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= 。如果对话正常返回,说明 Key 和端点都没问题,可以进入下一步搭约束骨架。

3. 可复制配置:AGENTS.md、CLAUDE.md 与 ESLint 三件套

3.1 AGENTS.md:跨代理通用指令

AGENTS.md 是给所有 AI 代理看的通用规则,不绑定具体工具。它应该短、可执行、指向明确。放在项目根目录,控制在 100 行左右:

# AGENTS.md — 跨代理通用指令 ## 项目速查 - 技术栈:Vue 3 + TypeScript + Vite + Pinia - 包管理器:pnpm - 测试:Vitest + Playwright ## 必须遵守的架构约束 - 层级依赖方向:types → config → repo → service → runtime → ui - 禁止反向依赖,types/ 不得 import 任何上层目录 - 所有 HTTP 请求必须走 src/api/client.ts,禁止直接 import axios - 组件命名使用 PascalCase,文件与组件同名 ## 可执行命令 - 安装:pnpm install - 开发:pnpm dev - 全量检查:pnpm check:all - 架构检查:pnpm check:arch - 文档新鲜度:pnpm check:docs ## 反馈循环 - 每次编辑后自动格式化(PostToolUse Hook) - 提交前跑 lint-staged - 大任务开始前先写 docs/execplans/ 计划 ## 文档索引 - 架构:docs/architecture.md - 规范:docs/conventions.md - 开发指南:docs/development-guide.md - 状态板:docs/harness-status.md

3.2 CLAUDE.md:Claude Code 主入口

CLAUDE.md 是 Claude Code 每次会话都会读的入口文件,它的定位是“目录”而不是“百科”。来自 242 个仓库的分析结论很一致:保持浅层级结构,一级标题加子节,不要深度嵌套,总行数控制在 100 行左右,超出部分放 docs/。

# 项目名 — Harness Guide ## Quick Reference Vue 3 / TypeScript strict / Vite / Pinia / Vitest ## Commands - pnpm dev — 启动开发服务器 - pnpm check:all — 格式 + lint + 架构 + 文档 + 构建 + 测试 - pnpm check:arch — 层级依赖检查 - pnpm check:docs — 文档引用路径新鲜度检查 ## Project Structure - src/types/ — 纯类型定义,无运行时依赖 - src/api/ — 请求封装,唯一出口 client.ts - src/stores/ — 状态管理 - src/components/ — 通用组件 - src/views/ — 页面级组件 ## Architecture Constraints - 依赖方向单向:types → api → stores → components → views - 禁止跨层反向 import - 禁止直接 import axios,统一走 api/client.ts ## Key Patterns - 组合式 API 优先,script setup 语法 - 状态用 Pinia,禁止在组件里直接改 store 外部状态 ## Documentation Index - docs/architecture.md — 系统架构与分层 - docs/conventions.md — 编码规范 - docs/development-guide.md — 开发工作流 - docs/harness-status.md — 当前目标与状态 ## CIVC Feedback Loops - Constrain:ESLint + TypeScript strict - Inform:本文件 + docs/ - Verify:pnpm check:all - Correct:PostToolUse 自动格式化

3.3 ESLint 架构约束:把规则变成机制

文档写了“禁止直接 import axios”,但 Agent 不一定记得住。用 ESLint 的no-restricted-imports把它变成硬约束,违反就报错:

// eslint.config.js import js from '@eslint/js' import tseslint from 'typescript-eslint' export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommended, { rules: { 'no-restricted-imports': ['error', { patterns: [ { group: ['axios'], message: '禁止直接 import axios,请使用 src/api/client.ts 封装。' }, { group: ['@/stores/*', '@/components/*', '@/views/*'], importNames: ['default'], message: 'types/ 层不得依赖上层模块,请检查依赖方向。' } ] }], '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }] } }, { files: ['src/types/**/*.ts'], rules: { 'no-restricted-imports': ['error', { patterns: [{ group: ['@/api/*', '@/stores/*', '@/components/*', '@/views/*'], message: 'types/ 是纯类型层,禁止 import 任何运行时模块。' }] }] } } )

3.4 架构检查脚本:补 ESLint 覆盖不到的方向

ESLint 管 import 语句,但管不了文件级别的层级方向。加一个 shell 脚本做兜底:

#!/bin/bash # scripts/check-architecture.sh # 检查 types/ 是否反向依赖了上层目录 VIOLATIONS=0 for file in src/types/*.ts; do if grep -qE "from ['\"]@/(api|stores|components|views)/" "$file"; then echo "VIOLATION: $file imports from forbidden layer" VIOLATIONS=$((VIOLATIONS + 1)) fi done exit $VIOLATIONS

挂进 package.json:

{ "scripts": { "check:arch": "bash scripts/check-architecture.sh", "check:all": "pnpm format:check && pnpm lint && pnpm check:arch && pnpm check:docs && pnpm build && pnpm test" } }

3.5 PostToolUse Hook:毫秒级纠正信号

Claude Code 每次编辑文件后触发 Hook,自动格式化并显式暴露失败。关键是不要吞错:

{ "hooks": { "PostToolUse": [ { "matcher": "Edit|MultiEdit|Write", "hooks": [ { "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/posttooluse-format.sh\"" } ] } ] } }

Hook 脚本本身:

#!/bin/bash # .claude/hooks/posttooluse-format.sh target_file=$(jq -r '.tool_input.file_path // empty' <<< "$CLAUDE_TOOL_INPUT") [ -z "$target_file" ] && exit 0 [ ! -f "$target_file" ] && exit 0 if npx prettier --write "$target_file" >/dev/null 2>&1; then printf '%s OK %s via=prettier\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$target_file" else status=$? printf '%s ERROR %s via=prettier exit=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$target_file" "$status" exit "$status" fi

反例是npx prettier --write "$target_file" >/dev/null 2>&1 || true,这会把格式化失败静默吞掉,日志和真实状态脱节,Agent 以为成功了其实没有。

4. 验证请求:从规则触发到校验通过的完整动作

配置写完不算完,要跑一次完整验证,确认约束真的生效。下面是一次从“故意违规”到“校验通过”的完整动作。

第一步,让 Claude Code 在src/types/user.ts里故意加一行违规 import:

// src/types/user.ts import { useUserStore } from '@/stores/user' // 故意违规 export interface User { id: string name: string }

第二步,跑架构检查:

pnpm check:arch

预期输出:

VIOLATION: src/types/user.ts imports from forbidden layer

退出码为 1,说明脚本正确拦截。

第三步,跑 ESLint:

pnpm lint

预期报错:

src/types/user.ts 1:1 error types/ 是纯类型层,禁止 import 任何运行时模块 no-restricted-imports

第四步,让 Claude Code 根据报错修复。因为报错信息里带了“问题 + 修复建议”,Agent 能直接理解并删掉违规 import,改用类型定义。

第五步,再跑全量检查:

pnpm check:all

预期全部通过,输出类似:

format:check OK lint OK check:arch OK check:docs OK build OK test OK

到这里,一次完整的 CIVC 闭环就跑通了:Constrain(ESLint 规则)→ Inform(报错信息)→ Verify(check:all)→ Correct(Agent 修复)。整个过程不需要人工介入,Agent 自己就能从失败信号里恢复。

如果你想让 Agent 长期跑编码任务,建议配合 Coding Plan 使用,把额度管理和任务编排放在一起:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= ,里面有各语言 SDK 和端点说明。

5. 本篇常见错排查

5.1 Hook 不触发

先确认.claude/settings.json里的 matcher 写的是Edit|MultiEdit|Write,大小写敏感。再确认$CLAUDE_PROJECT_DIR环境变量在 Claude Code 会话里能取到值。可以在 Hook 脚本开头加一行echo "hook fired: $target_file" >> /tmp/hook.log调试。

5.2 ESLint 规则不生效

no-restricted-imports的patterns里group用的是 glob 语法,@/stores/*能匹配@/stores/user,但匹配不到@/stores/user/index。如果项目用了路径别名,确认eslint.config.js里配了settings.import/resolver或者tsconfig的 paths 能被解析。

5.3 架构脚本误报

grep -qE "from ['\"]@/(api|stores|components|views)/"会匹配到注释里的字符串。如果 types 文件里有注释提到这些路径,会误报。改进方式是先剥离注释再 grep,或者用 TypeScript 的 AST 工具做更精确的检查。

5.4 CLAUDE.md 太长导致 Agent 忽略关键规则

超过 150 行后,Agent 对中间部分的注意力会下降。把详细内容移到docs/,CLAUDE.md 只保留命令、结构、约束和文档索引。用@docs/architecture.md这种引用方式让 Agent 按需加载。

5.5 文档引用路径失效

docs/里的文件引用了已经删除的源码路径,Agent 会按图索骥找不到文件。加一个check-docs-freshness.sh定期扫描:

#!/bin/bash # scripts/check-docs-freshness.sh stale=0 while IFS= read -r path; do clean="$(printf '%s' "$path" | sed -E 's/^[<(]+//; s/[>"),;:]+$//; s/:[0-9]+$//')" [ -z "$clean" ] && continue if [ ! -e "$clean" ]; then echo "STALE: $clean referenced in docs but not found" stale=$((stale + 1)) fi done < <( grep -rohE '(\.claude|docs|scripts|evals|observability|src)/[A-Za-z0-9_./:-]+' docs/*.md CLAUDE.md AGENTS.md | sort -u ) exit "$stale"

挂进check:all,每次全量检查都会扫一遍文档新鲜度。

5.6 Key 泄露进 git

.claude/settings.json里如果写了ANTHROPIC_API_KEY,很容易被提交。正确做法是 settings.json 只写ANTHROPIC_BASE_URL,Key 放.env或settings.local.json,并在.gitignore里排除。团队协作时用.env.example做模板,新人拉下来填自己的 Key。

6. 把约束骨架沉淀成长期资产

Harness Engineering 的价值不在于一次配置,而在于让约束成为仓库的一部分,随项目演进持续生效。AGENTS.md 和 CLAUDE.md 是告知层,ESLint 和架构脚本是约束层,Hook 和 check:all 是验证与纠正层,三者合起来才是完整的 CIVC 闭环。

实际落地时,建议从最小可用集开始:先写 CLAUDE.md 和 AGENTS.md,加一条no-restricted-imports规则,配一个 PostToolUse 格式化 Hook,跑通一次 check:all。等这套跑顺了,再逐步补架构脚本、文档新鲜度检查、evals 和 observability。不要一上来就搭七层架构,那样维护成本会压垮收益。

如果你还在选接入通道,TaoToken 的 API 端点是 https://taotoken.net/api ,Key 在控制台生成,Claude Code 只需要改ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY两个变量。模型对话验证在 https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= ,长期编码任务用 Coding Plan 更省心:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= 。接入细节看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content= 。

最后留一个实操建议:每次 Agent 出现重复性错误,不要只改 prompt,而是问自己“这个错误能不能用一条 ESLint 规则或一个脚本拦住”。能拦住的就写成机制,拦不住的才写进文档。这样跑上几周,你的仓库会自己长出护栏,Agent 的产出质量也会稳定下来。

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

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

立即咨询