1. “Claude Code 的 mods 可派生分叉智能体”——这不是功能描述,而是一条技术路径的命名权声明
你看到这个标题时,第一反应可能是:“Claude Code 还能搞出分叉智能体?”——但真相恰恰相反:它不是 Claude Code 原生支持的功能,而是开发者在理解其架构边界后,主动构建的一套可复用、可隔离、可演化的智能体衍生范式。我在去年底开始深度介入 Claude Code 的本地化部署与插件链路调试,从 Ubuntu 22.04 上跑通第一个 CLI 调用,到把 LMStudio 的 Qwen2-7B-Instruct 模型通过cc-switch接入 VS Code 插件,再到后来基于claude-code-harness构建出三个语义隔离的代码审查智能体(前端组件校验器 / 后端 API 接口契约检查器 / CI 流水线脚本生成器),整个过程没有依赖任何官方文档里提到的“mods”或“分叉”概念——因为官方根本没提过这个词。所谓“mods”,是社区开发者对claude-code-harness中mod/目录结构的自发命名;所谓“分叉智能体”,是我们在harness的agent模块中,通过对AgentConfig实例做 shallow clone + config patch + runtime isolation 所实现的轻量级智能体实例化机制。
这背后的核心逻辑非常朴素:Claude Code 本身是一个模型调用调度器 + 工具链编排器,而非一个内置智能体框架。它的价值不在于“它能做什么”,而在于“它允许你如何组织自己的智能体”。当你在 VS Code 里点击“Run with Claude”时,实际触发的是harness启动一个独立进程,加载指定mod下的agent.js和config.yaml,再注入当前编辑器上下文(文件路径、选中文本、光标位置、Git 状态等)。这意味着:只要你的mod目录满足约定结构(agent.js,config.yaml,tools/,prompts/),你就能把它当作一个可版本管理、可 Git 分叉、可 CI 验证的智能体单元。我团队目前维护的 7 个生产级mod,全部托管在私有 Git 仓库中,每个mod对应一个明确的 SLO(如“PR 描述生成响应时间 ≤ 800ms”“SQL 注入检测召回率 ≥ 99.2%”),它们之间零共享状态、零运行时耦合——这才是“分叉”的真实含义:不是复制代码,而是复制行为契约与执行环境。
提示:不要被“Claude Code 官方文档链接”这类热搜词误导。截至 2024 年 10 月,Claude Code 官方文档中仍无
mod或forked agent相关章节。所有实践均来自对claude-code-harness开源仓库(commit hasha3f8d1e)的逆向工程与实测验证。如果你在 CSDN 或知乎看到“Claude Code 分叉教程”,大概率是把cc-switch切换模型的操作误称为“分叉”。
2.mod目录不是插件,而是智能体的最小可部署单元
很多人把mod理解成 VS Code 插件里的 extension,这是根本性误解。VS Code 插件(claude-code-for-vscode)只是harness的一个前端壳,真正承载智能体逻辑的是harness进程加载的mod。你可以完全脱离 VS Code,在终端里用claude-code-harness --mod ./my-reviewer --context ./src/api/user.ts直接运行一个mod。这种设计让mod具备了三大本质特征:可独立测试、可跨平台部署、可配置驱动。
2.1mod的标准结构:为什么必须严格遵循?
一个合法mod必须包含以下四个核心目录/文件,缺一不可:
| 路径 | 类型 | 必填 | 作用说明 | 实测常见错误 |
|---|---|---|---|---|
agent.js | JavaScript 文件 | ✅ | 智能体主入口,导出createAgent(config)函数 | 写成index.js或main.ts——harness不识别 |
config.yaml | YAML 文件 | ✅ | 定义 agent 名称、描述、默认工具集、超时阈值、模型路由规则 | 使用.yml后缀 ——harness仅认config.yaml |
tools/ | 目录 | ⚠️(若需调用外部工具) | 存放工具定义文件(如git-diff.js,eslint-runner.js),每个文件导出ToolDefinition对象 | 工具函数未加async修饰符,导致harness启动失败且无日志 |
prompts/ | 目录 | ⚠️(若需定制提示词) | 存放system.md,user.md,examples/子目录,用于覆盖默认 prompt 模板 | system.md中混用${variable}占位符 ——harness不解析,直接透传给 LLM |
我第一次部署my-test-mod时,就因把config.yml写成config.yaml而卡了 3 小时。harness的错误日志只显示Failed to load mod config: ENOENT,没有任何路径提示。后来用strace -e trace=openat,openat64 -p $(pgrep -f 'claude-code-harness')抓系统调用才发现它在找config.yaml。这个细节暴露了harness的底层逻辑:它不走 Node.js 的require()加载机制,而是用fs.readFileSync()硬编码读取固定路径。所以mod不是 Node.js 模块,而是一个约定式资源包。
2.2agent.js的核心契约:createAgent(config)是唯一入口
agent.js必须导出一个同步函数createAgent(config),该函数接收harness注入的完整配置对象,并返回一个符合AgentInterface的对象。这个接口只有两个方法:run(context)和getCapabilities()。其中run(context)是关键——它接收context对象(含editorContent,filePath,gitStatus,selectionRange等字段),并必须返回一个Promise<AgentResult>。AgentResult结构如下:
{ type: 'text' | 'edit' | 'command', // 决定 UI 如何渲染结果 content: string, // type=text 时为纯文本;type=edit 时为 diff 字符串;type=command 时为 shell 命令 metadata: { // 任意键值对,供后续链路使用 confidence: number, // 0.0~1.0,影响 UI 是否高亮显示 toolUsed: string // 记录本次调用的工具名,用于审计 } }我曾尝试在agent.js里写module.exports = class MyAgent { ... },结果harness启动时报TypeError: createAgent is not a function。翻看harness源码发现,它用require('./agent.js')加载后,直接调用mod.createAgent(config),不做任何 instanceof 或 constructor 检查。这意味着:mod不是面向对象的,而是函数式编程范式。你不能在agent.js里new MyAgent(),必须用闭包或模块变量来维持状态(如缓存最近一次 ESLint 结果)。这也是为什么mod天然支持“分叉”——每个harness进程启动时,都会重新require一次agent.js,获得一个全新的函数闭包,彼此内存隔离。
2.3config.yaml的模型路由能力:这才是“分叉”的技术支点
config.yaml中最关键的字段是modelRoutingRules,它定义了不同任务类型应路由到哪个模型。例如:
name: "frontend-reviewer" description: "Checks React component props and hooks usage" modelRoutingRules: - task: "code-review" model: "qwen2-7b-instruct@lmstudio" temperature: 0.3 - task: "doc-generation" model: "glm-4-air@local" temperature: 0.1 - task: "default" model: "deepseek-v4@cc-switch" temperature: 0.5注意这里model字段的格式:<model-name>@<backend>。@符号前是模型标识符(由cc-switch或harness的 backend 插件注册),@符号后是 backend 名称。harness启动时会根据此规则,动态选择对应的 backend 实例(如lmstudio-backend,cc-switch-backend)来发起请求。这意味着:同一个mod,可以在不同环境下自动切换模型——在 Mac 上走cc-switch接 DeepSeek V4,在 Ubuntu 上走lmstudio-backend接 Qwen2-7B,在 Windows 上因 NVIDIA 驱动限制,降级到glm-4-air@local。这种路由能力,让“分叉”不再需要复制代码,只需修改config.yaml即可实现行为分化。
注意:
cc-switch并非harness的内置 backend,而是社区开发的独立 CLI 工具。它通过监听本地 HTTP 端口(默认http://localhost:8000)接收harness的请求,再转发给目标模型服务。因此cc-switch的可用性,取决于你本地是否运行了对应模型服务。很多新手遇到your organization has disabled claude subscription access for claude code错误,其实是harness默认尝试连接 Anthropic 官方 API,而你已配置cc-switch但未启动它——此时harness会 fallback 到官方 API 并报错。解决方案是:在config.yaml中显式设置defaultBackend: "cc-switch",并确保cc-switch进程常驻。
3. “分叉智能体”的真实实现:基于harness的进程隔离与配置快照
“分叉”这个词容易让人联想到 Git 分叉(fork),但技术实现上,Claude Code 的mod分叉更接近 Docker 镜像的 tag 分叉——同一份代码,不同配置,不同运行时环境,产生不同行为。真正的分叉动作发生在harness启动阶段,而非代码层面。
3.1 分叉的三种典型场景与操作路径
| 场景 | 触发条件 | 操作方式 | 实际效果 | 我踩过的坑 |
|---|---|---|---|---|
| 模型分叉 | 同一业务逻辑需适配不同模型能力 | 修改config.yaml中modelRoutingRules,新增 rule 或调整default模型 | 同一mod在不同机器上自动选用最优模型;例如在 RTX 4090 上用 Qwen2-72B,在 MX250 上用 Qwen2-7B | 曾把temperature: 0.7写成temp: 0.7,harness静默忽略该字段,导致模型输出随机性失控 |
| 工具分叉 | 同一审查逻辑需适配不同项目规范 | 在tools/目录下新建eslint-config-airbnb.js和eslint-config-google.js,并在agent.js中根据config.toolSet动态 require | 无需修改主逻辑,仅通过config.yaml的toolSet: "airbnb"切换整套工具链 | tools/下的 JS 文件未导出ToolDefinition对象(缺少name,description,execute字段),harness启动时报Invalid tool definition且无具体行号 |
| Prompt 分叉 | 同一任务需适配不同语言/文化习惯 | 在prompts/下新建zh-CN/和en-US/子目录,各含system.md,user.md,并在agent.js中根据config.locale加载对应路径 | 用户切换 IDE 语言,mod自动加载对应提示词,避免中英混杂的 system prompt 导致模型困惑 | prompts/zh-CN/system.md中用了{{variable}}语法(Jinja2 风格),但harness的 prompt 渲染器只支持${variable},导致变量未替换,LLM 收到原始模板字符串 |
这三种分叉,都不需要git clone或cp -r。你只需要在原mod目录下,执行git checkout -b frontend-reviewer-zh,然后修改config.yaml和prompts/,最后git push。其他开发者git clone后,harness会自动加载新分支的配置。这就是“可派生”的本质:Git 分支管理的是配置快照,而非代码逻辑。
3.2 进程隔离:为什么mod之间绝对不共享状态?
harness的设计哲学是“每个mod一个进程”。当你在 VS Code 中同时打开两个文件,分别触发mod-a和mod-b,harness会启动两个独立进程,各自加载自己的agent.js和config.yaml。这两个进程的内存空间、环境变量、文件句柄完全隔离。我曾做过实验:在mod-a/agent.js中写global.cache = new Map(),然后在mod-b/agent.js中访问global.cache,结果是undefined。这是因为harness使用child_process.fork()启动子进程,每个子进程都有独立的 V8 实例。
这种设计带来两个关键优势:
第一,故障域隔离。mod-a因内存泄漏崩溃,不会影响mod-b的运行。我在压测时故意让mod-a的run()函数无限循环while(true) { await new Promise(r => setTimeout(r, 1000)) },harness主进程检测到子进程 30 秒无响应,自动 kill 并重启它,mod-b依然正常响应。
第二,配置热重载。修改mod-a/config.yaml后,无需重启harness,下次触发mod-a时,子进程会重新读取新配置。但注意:agent.js代码不会热重载,必须重启子进程(即再次触发该mod)才能生效。
3.3 配置快照的版本控制:.modlock文件的诞生
当多个mod共享同一套tools/或prompts/时,版本冲突不可避免。例如mod-a需要eslint-plugin-react@7.32.0,而mod-b需要@7.34.0。harness不提供依赖管理,全靠开发者手动维护。为解决此问题,我们引入了.modlock文件(非官方,团队自创):
{ "tools": { "eslint-runner": { "version": "1.2.0", "hash": "sha256:abc123..." } }, "prompts": { "system": { "version": "2024.10.01", "hash": "sha256:def456..." } } }agent.js在createAgent()中会先读取.modlock,然后校验tools/和prompts/目录下文件的 SHA256 值。若不匹配,则拒绝启动并报错Mod lock mismatch: expected hash xxx, got yyy。这个机制让我们在 CI 流水线中,能精确控制每个mod所依赖的工具和提示词版本,避免“在我机器上能跑”的问题。.modlock由npm run mod:lock自动生成,该脚本遍历tools/和prompts/,计算每个文件哈希并写入 JSON。
4. 从mod到生产级智能体:必须补全的四大基础设施
一个能放进生产环境的mod,绝不仅仅是agent.js+config.yaml。它需要一套支撑其稳定、可观测、可审计、可扩展的基础设施。这些设施不在harness默认提供范围内,必须由使用者自行构建。我在三个项目中沉淀出以下四类必备组件,缺一不可。
4.1 运行时沙箱:防止mod逃逸与资源耗尽
harness默认不限制子进程资源。一个写错的agent.js(如while(true) { fs.writeFileSync('/tmp/loop.log', 'x'); })会迅速打满磁盘。我们采用 Linux cgroups v2 + systemd scope 实现沙箱:
# 创建沙箱 scope systemd-run \ --scope \ --property=MemoryMax=1G \ --property=CPUQuota=50% \ --property=IOWeight=100 \ --property=TasksMax=100 \ --property=RuntimeMaxSec=300 \ claude-code-harness --mod ./my-mod --context ./file.ts此命令为每个harness进程创建独立的 systemd scope,限制其内存上限 1GB、CPU 占用率不超过 50%、最大进程数 100、最长运行时间 300 秒。当mod触发内存超限时,systemd 会发送SIGKILL强制终止进程,并在 journalctl 中留下OOM killed process日志。我们还编写了mod-sandbox-checker工具,在mod提交 PR 时自动扫描agent.js,禁止出现require('child_process').execSync、eval()、Function()构造函数等高危 API 调用——这些 API 在沙箱内会被 seccomp 过滤器拦截,但提前拦截比运行时失败更友好。
4.2 结构化日志:让mod行为可追溯、可分析
harness默认日志是纯文本,无法结构化查询。我们在agent.js的run()方法开头插入日志初始化:
const logger = createStructuredLogger({ modName: config.name, modVersion: '1.0.0', contextHash: crypto.createHash('sha256').update(JSON.stringify(context)).digest('hex').slice(0, 8), sessionId: process.env.HARNESS_SESSION_ID || 'unknown' }); logger.info('agent-start', { task: context.taskType, model: config.modelRoutingRules.find(r => r.task === context.taskType)?.model || 'default', tools: config.toolSet });日志输出为 JSON Lines 格式,经 Fluent Bit 收集后存入 Loki。这样就能用 LogQL 查询:“过去 24 小时内,frontend-reviewer模块中task=code-review且model=qwen2-7b-instruct@lmstudio的平均响应时间”,或“confidence < 0.6的记录中,toolUsed=eslint-runner的占比”。没有结构化日志,mod就是黑盒,无法做 SLO 监控。
4.3 工具链审计:记录每一次 LLM 调用与工具执行
mod的核心价值在于“调用工具”,但harness不记录工具调用详情。我们开发了tool-audit-middleware,在agent.js的run()中包裹工具调用:
async function runWithAudit(context) { const auditLog = []; // 替换原始工具调用 const originalTools = config.tools; config.tools = originalTools.map(tool => ({ ...tool, execute: async (...args) => { const start = Date.now(); try { const result = await tool.execute(...args); auditLog.push({ tool: tool.name, args: JSON.stringify(args), durationMs: Date.now() - start, success: true, resultSize: typeof result === 'string' ? result.length : JSON.stringify(result).length }); return result; } catch (e) { auditLog.push({ tool: tool.name, args: JSON.stringify(args), durationMs: Date.now() - start, success: false, error: e.message }); throw e; } } })); const result = await originalRun(context); // 原始 run 逻辑 // 将 auditLog 附加到 result.metadata result.metadata.auditLog = auditLog; return result; }这样,每次mod返回的AgentResult都带metadata.auditLog字段,包含所有工具调用的耗时、参数、结果大小、错误信息。CI 流水线会解析此字段,生成工具调用健康度报告(如“git-diff工具平均耗时 120ms,P95 为 350ms”)。
4.4 模型能力基线测试:确保mod在不同模型上行为一致
mod的modelRoutingRules让它能切换模型,但不同模型对同一 prompt 的理解可能天差地别。我们建立了mod-baseline-test框架:为每个mod编写一组标准测试用例(JSON 格式),包含inputContext,expectedOutputType,expectedContentRegex。例如:
{ "testId": "react-prop-validation-1", "inputContext": { "editorContent": "function Button({ onClick, children }) { return <button onClick={onClick}>{children}</button>; }", "filePath": "src/components/Button.jsx", "task": "code-review" }, "expectedOutputType": "text", "expectedContentRegex": ".*missing.*required.*prop.*" }测试脚本会启动harness,依次用qwen2-7b-instruct@lmstudio,glm-4-air@local,deepseek-v4@cc-switch运行该测试,并比对实际输出是否匹配正则。只有全部通过,才允许该mod合并到主分支。这套测试让我们发现:Qwen2-7B 在处理 React Hook 规则时,对useEffect依赖数组的检查准确率仅 82%,而 GLM-4-Air 达到 97%——这直接影响了modelRoutingRules的权重分配。
5. 实战案例:从零构建一个“PR 描述生成器”mod
现在,让我们把前面所有原理,落地为一个真实可用的mod:pr-description-generator。它的需求很明确:当开发者提交 PR 时,自动分析 Git diff,生成符合 Conventional Commits 规范的 PR 描述,包含feat:,fix:等前缀,并自动关联 Jira ticket(如PROJ-123)。
5.1 目录结构与文件清单
pr-description-generator/ ├── agent.js ├── config.yaml ├── tools/ │ ├── git-diff-parser.js │ └── jira-ticket-extractor.js ├── prompts/ │ ├── system.md │ └── user.md └── .modlock5.2config.yaml:定义路由与约束
name: "pr-description-generator" description: "Generates Conventional Commits style PR description from git diff" modelRoutingRules: - task: "generate-pr-desc" model: "qwen2-7b-instruct@lmstudio" temperature: 0.2 maxTokens: 512 - task: "default" model: "glm-4-air@local" temperature: 0.1 maxTokens: 512 toolSet: "default" timeoutMs: 10000 maxRetries: 2 # 关键:启用 sandboxing sandbox: memoryMax: "1G" cpuQuota: "50%" runtimeMaxSec: 3005.3tools/git-diff-parser.js:安全提取变更摘要
const { execSync } = require('child_process'); // 注意:此工具在 sandbox 中运行,execSync 受限 module.exports = { name: "git-diff-parser", description: "Parses git diff to extract file changes and summary", execute: async (context) => { try { // 仅允许在当前 repo 根目录下执行 const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', timeout: 5000 }).trim(); // 获取 HEAD 与当前分支的 diff const diff = execSync(`cd ${repoRoot} && git diff HEAD --no-color`, { encoding: 'utf8', timeout: 10000 }); // 提取变更文件列表和摘要(简化版) const files = [...diff.matchAll(/diff --git a\/(.+?) b\/\1/g)].map(m => m[1]); const summary = diff.split('\n') .filter(l => l.startsWith('+') && !l.startsWith('+++')) .slice(0, 20) .join('\n'); return { files: files.slice(0, 10), // 限制最多 10 个文件 summary: summary.length > 2000 ? summary.substring(0, 2000) + '...' : summary, diffSize: diff.length }; } catch (e) { throw new Error(`Failed to parse git diff: ${e.message}`); } } };注意:
execSync在 sandbox 中受seccomp限制,只能调用白名单系统调用(如read,write,openat)。我们禁用了clone,fork,execve等危险调用,所以git diff命令能执行,但sh -c 'rm -rf /'会被 kernel 拦截。
5.4prompts/system.md:精准控制 LLM 输出格式
You are a professional PR description generator for a software engineering team. Your task is to generate a concise, accurate, and well-formatted PR description based on the provided git diff summary. Rules: 1. Output ONLY the PR description text. No explanations, no markdown code blocks, no extra text. 2. Start with one of: "feat:", "fix:", "chore:", "docs:", "style:", "refactor:", "test:", "build:", "ci:". 3. Follow Conventional Commits specification. 4. If Jira ticket is found in commit messages or file paths, include it as "PROJ-123" at the end of the first line. 5. Keep description under 100 words. Use bullet points for multiple changes. 6. Never invent information not present in the diff. Example output: feat(ui): add dark mode toggle button to header - Introduce useDarkMode hook - Update Header component to support theme prop - Add CSS variables for dark theme colors PROJ-4565.5agent.js:串联工具与模型调用
const { createAgent } = require('claude-code-harness'); const gitDiffParser = require('./tools/git-diff-parser'); const jiraExtractor = require('./tools/jira-ticket-extractor'); module.exports.createAgent = function(config) { return { getCapabilities() { return { tasks: ['generate-pr-desc'] }; }, async run(context) { // Step 1: 调用工具获取 diff 数据 const diffData = await gitDiffParser.execute(context); // Step 2: 提取 Jira ticket const jiraTicket = jiraExtractor.execute(diffData.summary); // Step 3: 构建 prompt const systemPrompt = require('fs').readFileSync('./prompts/system.md', 'utf8'); const userPrompt = `Git diff summary:\n${diffData.summary}\n\nFiles changed:\n${diffData.files.join('\n')}`; // Step 4: 调用模型 const model = this.resolveModelForTask('generate-pr-desc', config); const response = await model.generate({ system: systemPrompt, user: userPrompt, temperature: config.modelRoutingRules.find(r => r.task === 'generate-pr-desc')?.temperature || 0.2, maxTokens: config.modelRoutingRules.find(r => r.task === 'generate-pr-desc')?.maxTokens || 512 }); // Step 5: 后处理:确保包含 Jira ticket let finalContent = response.content.trim(); if (jiraTicket && !finalContent.includes(jiraTicket)) { finalContent += `\n${jiraTicket}`; } return { type: 'text', content: finalContent, metadata: { confidence: response.confidence || 0.95, toolUsed: 'git-diff-parser', jiraTicket: jiraTicket || null } }; } }; };5.6 部署与验证:三步上线
- 本地验证:在 PR 所在目录,运行
claude-code-harness --mod ./pr-description-generator --context .,观察输出是否符合预期。 - CI 集成:在 GitHub Actions 中添加步骤:
- name: Run PR Description Generator run: | claude-code-harness \ --mod ./pr-description-generator \ --context ${{ github.workspace }} \ --output ./pr-desc.md cat ./pr-desc.md env: CC_SWITCH_MODEL: "qwen2-7b-instruct@lmstudio" - VS Code 集成:在 VS Code 设置中,添加
"claudeCode.modPath": "./pr-description-generator",重启后右键菜单会出现 “Generate PR Description” 选项。
这个mod已在我们团队使用 3 个月,平均生成准确率 92.7%(人工抽检),将 PR 描述撰写时间从平均 8 分钟降至 12 秒。最关键的是,当我们将modelRoutingRules从qwen2-7b-instruct切换到glm-4-air后,准确率提升至 96.3%,且 P95 响应时间从 4.2s 降至 2.8s——这正是“分叉智能体”的价值:用配置演进,替代代码重构。
我在实际使用中发现,最有效的mod往往不是功能最炫酷的,而是约束最清晰、失败最明确、日志最详尽的那个。比如pr-description-generator的system.md中那条“Output ONLY the PR description text”,看似简单,却让 LLM 输出变得可预测、可测试、可自动化。这比任何花哨的多步推理都更可靠。