
1. 为什么我要把 Harness 源码拆开看AI Agent 工程化到底难在哪Harness 是一个用 TypeScript 写的 AI Agent 平台代码量在五十万行级别同时提供 CLI 和终端 UI 两套入口。它能做的事包括把大模型调用、工具执行、多 Agent 编排、会话状态管理统一到一个可扩展的框架里适合想自己搭 Agent 平台、又不想从零造轮子的开发者。我第一次翻它的目录时最直观的感受是这不是一个“调个 API 就完事”的脚本集合而是一套有明确分层、有类型约束、有启动性能考量的工程系统。很多人写 Agent 的起点是一个main.ts里面塞进 prompt、工具定义、循环调用跑通 demo 没问题一旦工具数量上到几十个、会话状态需要持久化、多个 Agent 要协作代码就会迅速失控。Harness 的价值就在于它把这些失控点提前用架构消化掉了。它用 TypeScript 的类型系统把工具输入输出钉死用 React 的组件模型去渲染终端界面用 CLI 入口做参数解析和生命周期管理。这三件事分别对应了“工具可扩展”“界面可维护”“启动可优化”三个工程目标。我关心的不是它有多少行代码而是它在关键分叉口做了什么取舍。比如工具系统为什么选择 fail-closed 而不是 fail-open启动阶段为什么要把预取和渲染拆成并行React 写终端 UI 到底解决了什么传统 readline 解决不了的问题。这些取舍才是“教科书”部分。下面我会按目录结构、类型系统、React 分层、CLI 入口、本地跑通、报错排查的顺序把可复制的部分交给你。你不需要读完五十万行只需要抓住那几个决定架构走向的文件和配置。2. 前置准备用 TaoToken 拿到可调用的模型入口在本地跑通 Harness 的 CLI 之前你需要一个能稳定调用的模型入口。Harness 本身是框架它不绑定某一家模型但你需要给它一个兼容 OpenAI 或 Anthropic 协议的 Base URL 和 Key。我这边用的是 TaoToken它的 API 地址是https://taotoken.net/api控制台在https://taotoken.net/consoleAPI Key 在https://taotoken.net/api-keys生成。官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。拿到 Key 之后你要做的是把它写进 Harness 的配置里。Harness 的配置通常放在项目根目录的harness.config.json或者环境变量里。我建议用环境变量因为 CLI 和 UI 两个入口都能读到不用改代码。你需要设置三个值Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiKey 填你生成的那串Model ID 填你要用的模型名比如claude-sonnet-4-20250514或者gpt-4o这类。如果你用的是 Claude Code 那套 Anthropic 协议Base URL 也是同一个Harness 内部会根据协议做适配。这里有个容易踩的坑很多人把 Base URL 写成带/v1的完整路径结果 Harness 内部又拼了一次/v1/chat/completions变成/v1/v1/...直接 404。TaoToken 的 API 根就是https://taotoken.net/api不要自己加/v1。另外Key 不要硬编码进 git 仓库用.env文件加.gitignore是最省事的做法。如果你还没生成 Key先去https://taotoken.net/api-keys建一个权限选最小可用即可。配置写好后你可以先用 curl 验证一下这个入口是通的再进 Harness。命令是curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: ping}] }如果返回里有choices字段说明入口没问题。这一步别跳过因为后面 Harness 报错时你要能区分是框架问题还是入口问题。入口通了再往下走。3. 可复制配置目录结构、类型系统与 CLI 入口的关键片段Harness 的目录结构大致是这样分层的src/cli放命令行入口和参数解析src/core放 Agent 循环、工具注册、会话状态src/tools放具体工具实现src/ui放 React 终端组件src/types放全局类型定义。这个分法的好处是依赖方向清晰cli 依赖 corecore 依赖 typesui 依赖 core 但不反向依赖 cli。你在自己项目里可以照搬这个骨架哪怕不用 React把ui换成render也行。工具系统的类型定义是重点。Harness 里每个工具都要声明输入和输出的 schema用 TypeScript 的泛型把execute函数的参数类型和 schema 绑死。这样模型传错参数时编译期就能发现而不是运行到一半才崩。一个简化的工具定义长这样// src/types/tool.ts export interface ToolDefinitionInput, Output { name: string; description: string; inputSchema: z.ZodTypeInput; execute: (input: Input, ctx: ToolContext) PromiseOutput; // fail-closed: 校验失败直接拒绝不降级执行 onValidationError: reject; }注意onValidationError: reject这个字段这就是 fail-closed 的体现。很多框架默认是 fail-open参数不对就忽略继续跑结果 Agent 拿着错误结果往下走最后报一个莫名其妙的错。Harness 选择直接拒绝把问题暴露在第一现场。你在自己项目里也应该这样宁可报错清晰不要静默降级。CLI 入口的设计也值得抄。Harness 的src/cli/index.ts里启动阶段做了三件事的并行预取读配置、加载工具注册表、初始化模型客户端。这三件事互不依赖串行做会白白多等几百毫秒。代码结构大概是// src/cli/index.ts const [config, tools, client] await Promise.all([ loadConfig(), loadToolRegistry(), createModelClient(), ]); const agent createAgent({ config, tools, client }); await agent.run(parseArgs(process.argv));Promise.all这里不是炫技是实打实的启动优化。工具注册表如果从磁盘读几十个文件串行读会明显拖慢冷启动。并行预取之后启动时间能压到原来的三分之一左右。你如果工具不多可以先不并行但结构上留好这个位置以后工具多了直接改。React 写终端 UI 这部分Harness 用的是ink这类库把组件树渲染成终端字符。它的分层是App组件管全局状态MessageList管消息渲染InputBox管用户输入StatusBar管模型和 token 状态。这样拆的好处是状态变化时只有相关组件重渲染不会整个终端刷一遍。传统 readline 写法里你手动清屏、手动定位光标消息一多就闪屏。React 的 diff 机制把这个问题解决了。你如果不想引入 React至少要把“状态”和“渲染”分开别在渲染函数里直接改状态。配置片段方面Harness 支持harness.config.json关键字段是model.baseUrl、model.apiKey、model.modelId、tools.registryPath、ui.theme。我建议把apiKey留空用环境变量TAOTOKEN_API_KEY注入配置文件里只写apiKeyEnv: TAOTOKEN_API_KEY。这样配置文件可以进 gitKey 不会泄露。工具注册表路径指向src/tools/index.ts导出的数组即可。4. 验证请求本地跑通 CLI 并确认 Agent 调用链配置写好后先装依赖再跑。Harness 用 pnpm 管理依赖命令是pnpm install然后pnpm build编译 TypeScript。编译通过后用node dist/cli/index.js --help看 CLI 是否正常输出帮助信息。如果这一步报模块找不到多半是tsconfig.json的outDir和package.json的bin路径对不上检查一下。跑通 CLI 之后用一条最简单的指令验证 Agent 调用链node dist/cli/index.js run 列出当前目录的文件。这条指令会触发模型调用模型返回一个工具调用请求Harness 执行list_files工具把结果回传给模型模型再生成最终回答。你会在终端看到三段输出模型思考、工具执行、最终回复。如果只看到模型思考就停了说明工具注册没生效检查tools.registryPath指向的文件是否真的导出了工具数组。验证调用链是否完整最直接的方法是看日志。Harness 在src/core/agent.ts里打了几个关键日志点agent:request、agent:tool_call、agent:tool_result、agent:response。你可以在启动时加--verbose参数把这些日志打出来。如果agent:tool_call有但agent:tool_result没有说明工具执行抛异常了去看工具内部的 try/catch。如果agent:request有但agent:tool_call没有说明模型没返回工具调用可能是 prompt 里没把工具描述清楚或者模型不支持 function calling。我实测下来最容易出问题的是模型返回的 tool_call 参数格式和 schema 对不上。比如 schema 要求path是 string模型返回了{path: {value: /tmp}}。这时候 fail-closed 就会拒绝日志里会打validation_error。你要做的是把 schema 写得更明确或者在工具描述里给一个参数示例。Harness 的工具描述支持 markdown你可以在描述里写path: string, e.g. /home/user模型看到示例后返回正确格式的概率会高很多。还有一个验证点是多轮对话。跑node dist/cli/index.js chat进入交互模式连续问两个有关联的问题比如先问“当前目录有哪些文件”再问“把第一个文件的内容读出来”。如果第二轮能正确引用第一轮的结果说明会话状态管理是通的。Harness 把会话状态存在src/core/session.ts里用Map按 sessionId 隔离。你如果发现第二轮丢了上下文检查 sessionId 是否在每轮请求里都带上了。5. 常见报错排查401、local proxy failed、reading choices、OAuth第一个高频报错是401 Unauthorized。这个基本就是 Key 的问题。先确认TAOTOKEN_API_KEY环境变量在当前 shell 里真的存在用echo $TAOTOKEN_API_KEY看输出。如果输出为空说明你没 export或者写在了.env但没加载。Harness 不会自动读.env你要么用dotenv包在入口处加载要么手动export。另外Key 如果复制时带了空格或换行也会 401重新生成一个再试。第二个报错是local proxy failed。这个通常出现在你本地起了代理但 Harness 的请求没走代理或者代理配置和 Base URL 冲突。TaoToken 的 API 是直连的不需要额外代理。如果你系统里设了HTTP_PROXY或HTTPS_PROXY环境变量Harness 底层的 fetch 可能会走代理导致连接失败。解决办法是临时清掉这两个环境变量unset HTTP_PROXY HTTPS_PROXY再跑一次。如果清了就好了说明是代理干扰不是框架问题。第三个报错是Cannot read properties of undefined (reading choices)。这个说明模型返回的响应结构里没有choices字段。常见原因有三个一是 Base URL 写错了请求打到了某个返回 HTML 的地址解析 JSON 失败二是模型名写错了服务端返回了错误对象而不是正常响应三是流式和非流式模式搞混了Harness 按非流式解析但服务端返回了 SSE 流。你先用第 2 节的 curl 命令确认入口返回正常再检查 Harness 里的model.modelId是否和 curl 里用的一致。第四个报错是OAuth token expired或OAuth callback failed。这个一般出现在你用 Claude Code 那套 Anthropic 协议接入时。Anthropic 的 OAuth 流程需要浏览器回调如果你在无头环境或者回调地址被占用就会失败。解决办法是改用 API Key 模式不走 OAuth。在 Harness 配置里把model.authType设成api_key然后填apiKeyEnv。TaoToken 的 API Key 模式不需要 OAuth直接 Bearer 认证省掉回调这一步。还有一个隐蔽的报错是工具执行超时但没有日志。Harness 默认工具超时是 30 秒超时后抛ToolTimeoutError但如果你在工具内部自己 catch 了所有异常这个错误就被吞了。检查你的工具execute函数别写catch (e) {}这种空捕获。要么不 catch让错误冒泡到 Agent 层要么 catch 之后重新抛一个带上下文的错误。我踩过的坑就是工具里空 catch结果 Agent 一直等最后报一个和真实原因无关的错。排查顺序建议是先 curl 验入口再跑 CLI 看日志再看工具注册最后看会话状态。每一步都确认了再往下别跳步。大部分问题都出在入口和配置真正框架内部的 bug 很少。6. 接下来怎么用从跑通到长期编码跑通 CLI 只是第一步。如果你打算把 Harness 这类框架用在日常编码或者 Agent 开发上长期来看你需要一个稳定的模型调用方案。TaoToken 的 Coding Plan 适合这种场景入口在https://taotoken.net/coding-plan。它和按量计费的 API Key 是两套东西Coding Plan 更偏向固定周期内的编码用量适合你每天都要跑 Agent、调工具链的情况。你可以先去模型对话页面https://taotoken.net/chat试试模型响应质量确认没问题再上 Coding Plan。接入文档在https://taotoken.net/doc里面写了不同协议OpenAI 兼容、Anthropic 兼容的 Base URL 和认证方式。Claude Code 相关的接入说明在https://taotoken.net/claude-code如果你用 Claude Code 作为 Harness 的模型后端这个页面有完整的配置步骤。API Key 管理在https://taotoken.net/api-keys控制台在https://taotoken.net/console。我的建议是先用 API Key 模式把 Harness 跑通验证工具调用链和会话状态都没问题再根据你的日常用量决定要不要上 Coding Plan。别一上来就买套餐先跑通再说。跑通之后你可以把 Harness 的目录结构抄到你自己的项目里把工具系统换成你的业务工具把 React UI 换成你需要的界面。框架的价值在于它帮你把工程化的坑填了你只需要关注业务逻辑。