
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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台生成 Key。API 基地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接填进配置即可。Claude Code 的接入方式是在项目根目录或全局环境里设置两个变量。macOS / Linux 下写入 shell 配置export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYsk-你的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_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_content 。注意Key 不要提交进 git。把.env和.claude/settings.local.json加进.gitignore仓库里只保留.env.example模板。接入完成后先用一次模型对话验证通道是否通https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_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.md3.2 CLAUDE.mdClaude 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 - ConstrainESLint TypeScript strict - Inform本文件 docs/ - Verifypnpm check:all - CorrectPostToolUse 自动格式化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/ 是否反向依赖了上层目录 VIOLATIONS0 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 21; then printf %s OK %s viaprettier\n $(date -u %Y-%m-%dT%H:%M:%SZ) $target_file else status$? printf %s ERROR %s viaprettier exit%s\n $(date -u %Y-%m-%dT%H:%M:%SZ) $target_file $status exit $status fi反例是npx prettier --write $target_file /dev/null 21 || 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说明脚本正确拦截。第三步跑 ESLintpnpm 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 闭环就跑通了ConstrainESLint 规则→ Inform报错信息→ Verifycheck:all→ CorrectAgent 修复。整个过程不需要人工介入Agent 自己就能从失败信号里恢复。如果你想让 Agent 长期跑编码任务建议配合 Coding Plan 使用把额度管理和任务编排放在一起https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_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 stale0 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_URLKey 放.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_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_content 长期编码任务用 Coding Plan 更省心https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_content 。接入细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_content 。最后留一个实操建议每次 Agent 出现重复性错误不要只改 prompt而是问自己“这个错误能不能用一条 ESLint 规则或一个脚本拦住”。能拦住的就写成机制拦不住的才写进文档。这样跑上几周你的仓库会自己长出护栏Agent 的产出质量也会稳定下来。