☰
Claude Code 工程化实战:从 Harness 到 Skills,拆解爆款 Agent 的设计密码
2026/10/9 21:28:05 网站建设 项目流程

1. 从爆款 Agent 源码里,我到底该抄什么

Claude Code 工程化实战这件事,很多人卡在一个误区:把爆款 Agent 的源码当成功能清单来抄。看到 OpenClaw 有 Telegram 通道就加一个,看到 Hermes 用 TypeScript 写配置就跟着换,结果项目越堆越乱,跑起来还是三天两头报错。问题不在抄,在于没搞清楚这些项目真正做对的是什么。

Claude Code 本身是一个具体的 CLI 产品,而 OpenClaw、Hermes 这类项目是围绕 Agent 运行时搭起来的骨架。它们能被大量团队拿去改造成自己的东西,靠的不是某个炫技功能,而是三条主线:Harness 负责编排 Agent 的执行循环,Channel 负责把不同来源的消息接进来,Skills 负责把能力做成可复用、可发现、可降级的模块。这三条线合起来,就是一套可维护的 Agent 骨架。

这篇文章面向的是已经用过 Claude Code、想把它工程化落地到自己项目里的开发者。我会给出一套可以直接复制的目录结构、配置片段和本地验证步骤,让你在自己的仓库里跑通 Harness + Channel + Skills 的最小闭环。读完之后,你应该能判断一个爆款 Agent 的哪些设计值得搬、哪些只是它自己的历史包袱。

先说清楚一个前提:下面所有配置和代码都是本地可跑的骨架,不依赖任何特殊网络环境。你需要准备的只有 Node.js 18+、一个可用的模型 API Key,以及一个空目录。模型接入部分我会用 TaoToken 作为示例,因为它同时提供 Claude 系列和 OpenAI 兼容接口,配置起来比较省事,但换成任何兼容 OpenAI 协议的服务商,下面的结构都成立。

我试过把 OpenClaw 的 7 层骨架直接照搬到一个小项目里,结果是过度设计——一个内部工具根本不需要 7 层。后来砍到 3 层:runtime、channels、skills,反而跑得更稳。所以下面的骨架是精简版,保留最核心的三条主线,你可以按需往上加。

2. TaoToken 前置准备:把模型接入这步先跑通

在写 Harness 之前,得先有一个能稳定调用的模型端点。这一步很多人会跳过,直接开始写 Agent 循环,结果调试时报错分不清是编排逻辑的问题还是模型接入的问题。所以先把接入这步单独验证一遍。

TaoToken 的接入方式兼容 OpenAI 协议,这意味着你不需要为它单独写一套 SDK 适配层。它的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 base URL 使用。你需要在控制台创建一个 API Key,创建入口在https://taotoken.net/console,登录后进 API Keys 页面新建即可。

拿到 Key 之后,先别急着写代码,用 curl 验证一次,确认 Key 和网络都正常:

curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [ {"role": "user", "content": "只回复两个字:通了"} ], "max_tokens": 32 }'

如果返回的 JSON 里choices[0].message.content是「通了」,说明接入没问题。这一步失败的话,后面所有 Harness 调试都是白费功夫,所以务必先过。

关于模型 ID,TaoToken 支持 Claude 系列和 OpenAI 系列,具体可用列表在文档里能查到,地址是https://taotoken.net/doc。我一般会在项目里把模型 ID 抽成环境变量,而不是硬编码在代码里,这样切换模型不用改代码。

环境变量建议这样组织,放在项目根目录的.env里:

TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api AGENT_MODEL=claude-sonnet-4-20250514 AGENT_MODEL_FAST=claude-haiku-4-5-20251001

这里我准备了两个模型 ID:一个主力模型跑复杂推理,一个快速模型跑分类、路由这类轻量任务。这是从 Hermes 的配置里学来的——它给 default 和 reviewer 配了不同模型,reviewer 用更便宜的模型做代码审查,成本能降一大截。

如果你打算长期跑编码类 Agent,可以考虑 Coding Plan,它在高频调用场景下比按量计费更划算,入口在https://taotoken.net/coding-plan。不过对于本文的骨架验证,按量计费完全够用,先跑通再说。

有一点要提醒:不要把 API Key 提交到 git。.env必须进.gitignore,团队协作时用.env.example提供模板。这个坑我踩过,Key 泄露后只能紧急轮换,很麻烦。

3. 可复制配置:Harness + Channel + Skills 三件套

现在进入正题。下面这套目录结构是我从 OpenClaw 和 Hermes 的骨架里精简出来的,保留了三条主线,去掉了平台化项目才需要的部署层和前端层。

my-agent/ ├── package.json ├── tsconfig.json ├── .env ├── .env.example ├── .gitignore ├── src/ │ ├── harness/ │ │ ├── loop.ts # Agent 执行循环 │ │ ├── types.ts # 核心类型定义 │ │ └── hooks.ts # PreToolUse / PostToolUse 钩子 │ ├── channels/ │ │ ├── interface.ts # Channel 抽象接口 │ │ ├── cli.ts # 命令行通道 │ │ └── http.ts # HTTP 通道 │ ├── skills/ │ │ ├── registry.ts # Skills 注册中心 │ │ ├── read-file.ts # 读文件 Skill │ │ └── run-shell.ts # 执行命令 Skill │ └── index.ts # 入口 └── agent.config.ts # 配置即代码

先看agent.config.ts,这是整个骨架的配置中心。我采用 Hermes 的「配置即代码」范式,用 TypeScript 写配置而不是 YAML,好处是能享受类型检查和自动补全:

// agent.config.ts import { defineConfig } from "./src/harness/types"; export default defineConfig({ model: { baseUrl: process.env.TAOTOKEN_BASE_URL!, apiKey: process.env.TAOTOKEN_API_KEY!, default: process.env.AGENT_MODEL!, fast: process.env.AGENT_MODEL_FAST!, }, harness: { maxTurns: 12, maxTokensPerTurn: 4096, stream: true, hooks: { PreToolUse: "./src/harness/hooks.ts", }, }, channels: { cli: { enabled: true }, http: { enabled: true, port: 8787 }, }, skills: { dir: "./src/skills", autoDiscover: true, }, });

这个配置里几个关键点值得说明。maxTurns限制 Agent 最多循环多少轮,防止它在某个任务上无限打转;stream: true开启流式输出,这是所有交互式 Agent 的标配,用户不用盯着屏幕等 30 秒;hooks.PreToolUse指向一个钩子文件,用来在工具执行前做拦截,比如挡住危险命令。

再看 Harness 的核心类型定义,这是整个骨架的地基:

// src/harness/types.ts export interface ToolSchema { name: string; description: string; input: Record<string, unknown>; risk: "low" | "medium" | "high"; execute: (args: Record<string, unknown>) => Promise<unknown>; } export interface Channel { name: string; start: (onMessage: (text: string) => Promise<string>) => Promise<void>; } export interface AgentConfig { model: { baseUrl: string; apiKey: string; default: string; fast: string; }; harness: { maxTurns: number; maxTokensPerTurn: number; stream: boolean; hooks?: Record<string, string>; }; channels: Record<string, { enabled: boolean; port?: number }>; skills: { dir: string; autoDiscover: boolean }; } export function defineConfig(config: AgentConfig): AgentConfig { return config; }

注意ToolSchema里的risk字段。这是从 Hermes 学来的「工具白盒化」——把工具的危险等级显式声明出来,Agent 在自主选工具时可以参考,钩子也能据此拦截。对比只靠 deny 规则拦的做法,显式声明更清晰。

Channel 接口设计得很薄,只有一个start方法,接收一个消息处理函数。这就是「输入适配器」模式:Agent 不关心消息从 CLI 来还是从 HTTP 来,只管收到一条消息、返回一个回复。CLI 通道和 HTTP 通道各自实现这个接口即可。

Skills 注册中心负责扫描skills目录、加载所有工具、生成给模型看的工具描述。这里有个细节:工具的description是模型选工具的唯一依据,必须写清楚。写「读文件」远不如写「读取文件内容,返回带行号的文本,适合查看代码或配置」有用。这个坑我在早期项目里踩过,工具描述太简略,模型经常选错工具。

4. 验证请求:本地跑通一次完整 Agent 循环

配置写完了,现在验证它能不能跑。先装依赖:

npm init -y npm install openai dotenv npm install -D typescript tsx @types/node

然后写 Harness 的执行循环。这是整个骨架的心脏,逻辑不复杂,但要处理好流式输出和工具调用:

// src/harness/loop.ts import OpenAI from "openai"; import type { ToolSchema } from "./types"; export async function runAgentLoop( userInput: string, tools: ToolSchema[], config: { baseUrl: string; apiKey: string; model: string; maxTurns: number } ): Promise<string> { const client = new OpenAI({ baseURL: config.baseUrl, apiKey: config.apiKey, }); const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "system", content: "你是一个可调用工具的 Agent,按需选择工具完成任务。" }, { role: "user", content: userInput }, ]; const toolDefs = tools.map((t) => ({ type: "function" as const, function: { name: t.name, description: t.description, parameters: t.input, }, })); for (let turn = 0; turn < config.maxTurns; turn++) { const resp = await client.chat.completions.create({ model: config.model, messages, tools: toolDefs.length > 0 ? toolDefs : undefined, max_tokens: 4096, }); const choice = resp.choices[0]; const msg = choice.message; messages.push(msg); if (!msg.tool_calls || msg.tool_calls.length === 0) { return msg.content ?? ""; } for (const call of msg.tool_calls) { const tool = tools.find((t) => t.name === call.function.name); if (!tool) { messages.push({ role: "tool", tool_call_id: call.id, content: `错误:未找到工具 ${call.function.name}`, }); continue; } try { const args = JSON.parse(call.function.arguments); const result = await tool.execute(args); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result), }); } catch (err) { messages.push({ role: "tool", tool_call_id: call.id, content: `工具执行失败:${(err as Error).message}`, }); } } } return "达到最大轮次限制,任务未完成。"; }

这段代码有几个设计取舍值得说。第一,工具执行失败时不抛异常,而是把错误信息作为 tool 消息塞回对话,让模型自己决定重试还是换工具——这就是「错误可恢复」范式。第二,每轮把 assistant 消息 push 进 messages,保证上下文完整。第三,maxTurns兜底,防止死循环。

接着写一个读文件的 Skill 来验证工具调用链路:

// src/skills/read-file.ts import { readFile } from "node:fs/promises"; import type { ToolSchema } from "../harness/types"; export const readFileTool: ToolSchema = { name: "read_file", description: "读取指定路径的文件内容,返回带行号的文本,适合查看代码或配置。", risk: "low", input: { type: "object", properties: { path: { type: "string", description: "文件的绝对路径" }, }, required: ["path"], }, async execute(args) { const path = args.path as string; const content = await readFile(path, "utf-8"); const lines = content.split("\n").map((l, i) => `${i + 1}\t${l}`); return { path, total_lines: lines.length, content: lines.join("\n") }; }, };

最后写入口,把 Harness 和 Skill 串起来:

// src/index.ts import "dotenv/config"; import { runAgentLoop } from "./harness/loop"; import { readFileTool } from "./skills/read-file"; async function main() { const result = await runAgentLoop( "读一下 package.json,告诉我项目名和依赖数量", [readFileTool], { baseUrl: process.env.TAOTOKEN_BASE_URL!, apiKey: process.env.TAOTOKEN_API_KEY!, model: process.env.AGENT_MODEL!, maxTurns: 8, } ); console.log("\n=== Agent 最终回复 ==="); console.log(result); } main().catch(console.error);

跑起来:

npx tsx src/index.ts

如果一切正常,你会看到模型先调用read_file读取 package.json,拿到内容后总结出项目名和依赖数量。这就是一个最小可用的 Agent 循环——Harness 编排、Skill 提供能力,Channel 暂时用 CLI 入口代替。

想验证 HTTP 通道的话,把src/channels/http.ts补上,用 Node 内置的http模块起一个服务,收到 POST 请求就调runAgentLoop,返回结果。这样你的 Agent 就能被其他系统调用了。Channel 的价值就在这里:同一套 Harness,换个入口就能接不同来源的消息。

5. 本篇常见错排查:401、工具不触发、循环打转

骨架跑起来之后,报错基本集中在这几类。我把真实遇到过的错误和排查路径列出来,你对照着看。

401 Unauthorized。这是最常见的,八成是 Key 没读到。先确认.env文件在项目根目录,且import "dotenv/config"在入口文件最顶部——如果它排在runAgentLoop的 import 之后,环境变量还没加载就被读取了,拿到的是 undefined。再确认 Key 没有多余空格,复制粘贴时经常带上换行。最后用第 2 节的 curl 命令单独验证 Key 本身是否有效,排除 Key 被禁用或额度耗尽的情况。

工具不触发,模型直接回答。模型没调工具,通常是description写得太模糊,或者input的 JSON Schema 有问题。检查parameters里type是不是"object",properties和required是否对应。另外,如果tools数组为空,toolDefs就是空数组,传给 API 时会被忽略,模型自然不调工具。还有一种情况:模型觉得这个问题不需要工具就能答,比如你问「1+1 等于几」,它不会去读文件。换个明确需要工具的任务再测。

循环打转,达到 maxTurns。模型反复调同一个工具、拿不到有用结果,就会一直转。排查方向:工具返回的内容是不是模型看不懂的格式?比如返回了一个嵌套很深的 JSON,模型解析不了就会重试。把工具返回值拍平成简单结构,或者加一个summary字段用自然语言描述结果。另一个原因是工具执行一直失败,错误信息又不够明确,模型不知道该怎么改。确保错误信息里带上具体原因,比如「文件不存在:/path/to/x」而不是「读取失败」。

local proxy failed / connection refused。这类错误说明请求根本没发出去。检查baseUrl是不是写成了https://taotoken.net/api/v1还是https://taotoken.net/api——OpenAI SDK 会自动在 baseURL 后面拼/chat/completions,所以 baseURL 应该到/api为止,不要带/v1。如果你在本地配了 HTTP 代理,确认代理没有拦截这个域名。企业网络环境下,防火墙可能挡了出站请求,这个需要找网络管理员确认。

reading 'choices' of undefined。这个报错说明 API 返回的结构和预期不符,通常是请求本身失败了但没抛异常。打印完整的resp看看,常见原因是模型 ID 写错了,服务端返回了一个错误对象而不是正常的 completion 结构。对照文档确认模型 ID 拼写,注意大小写和日期后缀。

OAuth / 认证方式混淆。如果你之前用过 Claude Code 的 OAuth 登录,可能会想当然地以为 API 调用也走 OAuth。不是的,API 调用走的是 Bearer Token,就是你在控制台创建的 API Key。这两套认证是独立的,别混用。Claude Code 的 OAuth 是给 CLI 工具本身用的,你的 Agent 代码里用的是 API Key。

排查时有个通用技巧:在runAgentLoop里把每轮的messages打印出来,看模型到底收到了什么、返回了什么。大部分问题看一眼对话历史就清楚了。这个调试开关建议做成环境变量控制,生产环境关掉。

6. 把范式带回自己的项目

跑通最小闭环之后,下一步是把这套骨架扩展成你项目真正需要的样子。这里给几条实操建议,都是从爆款项目里提炼出来的。

Skills 的发现机制值得做扎实。现在autoDiscover只是扫描目录,你可以进一步给每个 Skill 加tags和version字段,让注册中心支持按标签筛选、按版本降级。OpenClaw 的 Skills 注册中心就是这么做的,当某个 Skill 加载失败时,它会自动降级到上一个可用版本,而不是整个 Agent 崩掉。

Channel 的抽象要守住。我见过不少项目一开始只做 CLI,后来要加 Web 入口时,把 Harness 逻辑复制了一份到 HTTP handler 里,结果两套逻辑逐渐分叉,改一个 bug 要改两处。正确做法是 Harness 只暴露一个runAgentLoop函数,所有 Channel 都调它,Channel 层只负责消息的收发和格式转换。

Hooks 是审计和安全的抓手。PreToolUse钩子可以在工具执行前检查参数,比如挡住rm -rf这类危险命令,或者检测参数里有没有敏感信息。PostToolUse钩子可以记录每次工具调用的输入输出,方便事后回放。企业场景下这两个钩子是刚需,早点留好扩展点。

模型分层能省不少成本。把路由、分类、简单问答交给快速模型,复杂推理和代码生成交给主力模型。Hermes 的 reviewer agent 用便宜模型做代码审查就是这个思路。你可以在 Harness 里加一个routeModel函数,根据任务类型选模型,配置里已经预留了default和fast两个 ID。

最后说一个心态问题。读爆款源码的价值不在于抄功能,而在于理解它们为什么这么设计。OpenClaw 做 7 层是因为它要支撑平台化生态,你的内部工具做 3 层就够了。Hermes 用 TypeScript 写配置是因为它面向工程团队,你的个人项目用 JSON 也没问题。关键是搞清楚每个设计选择背后的约束,然后判断这个约束在你的场景里成不成立。

这套骨架你可以直接 clone 下来改,也可以只挑 Harness 那部分嵌进现有项目。跑通之后,建议你拿一个真实的小任务测一测,比如「扫描 src 目录,找出所有超过 500 行的文件并列出文件名」。这种任务需要多轮工具调用,能检验 Harness 的循环、Skill 的协作和错误恢复是否都正常。测通了,你就有了一个可以持续往上加能力的 Agent 底座。

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

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

立即咨询