尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入 Claude Code 源码(八):自定义工具——给 Agent 装上专属「手」

深入 Claude Code 源码(八):自定义工具——给 Agent 装上专属「手」 1. 为什么通用工具不够用从一次“猜命令”的翻车说起Claude Code 自带的 Bash、Read、Write、Grep 这些工具本质上是“通用手”。它们能干活但不知道你业务里的语义。我试过让 Agent 去查公司内网知识库它老老实实拼了一长串 curl再自己写 sed 解析 JSON中间字段名猜错两次最后返回一堆乱码还自信地总结“未找到相关内容”。问题不在模型笨而在于它只能靠猜——它不知道“这个操作叫查询知识库”只知道“我在执行一条命令”。自定义工具要解决的就是这件事给 Agent 装上一只专属的“手”并且明确告诉它这只手叫什么、什么时候伸、伸出去抓什么。Claude Code 提供了两条路一条是 SDK 内嵌 MCP Server进程内用tool()createSdkMcpServer()另一条是独立 MCP Server进程外注册到settings.json。本篇聚焦前者把settings.json骨架、工具注册链路、端到端验证一次讲透同时说明 TaoToken 统一 Key/API 通道该接在哪一层。适合谁看已经跑通过 Claude Code 基础 Agent、想给 Agent 加业务专属能力的开发者正在用 SDK 构建 Agent、被“临时拼命令”折磨的人以及想把工具跨项目复用、准备上 MCP Server 的人。2. TaoToken 前置统一 Key 与 API 通道接在哪一层在动手写工具之前先把“模型从哪来”这件事定下来。Claude Code SDK 默认走 Anthropic 官方通道但很多团队希望用统一入口管理 Key、额度和调用记录。TaoToken 在这里扮演的是 API 通道角色你拿一个统一 KeySDK 侧通过环境变量指向它的 API 地址模型对话、Coding Plan、工具调用都走同一条链路。需要说清楚的是TaoToken 不是编辑器替代品也不碰你的本地代码它只负责模型请求的转发与计量。自定义工具的执行逻辑仍然跑在你自己的 Node/Bun 进程里工具里访问内网 API、SQLite 的凭证也由你自己保管不经过模型通道。接入位置很明确在 SDK 初始化前设置环境变量让query()发出的模型请求走 TaoToken。先到控制台创建 Key再确认通道地址。# 1. 获取统一 Key在控制台创建后复制 # 控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite # 2. 写入环境变量建议放 .env不要提交到 git export TAOTOKEN_API_KEYsk-你的统一Key export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY$TAOTOKEN_API_KEY注意ANTHROPIC_BASE_URL只写到/api不要带任何查询参数。Key 用环境变量注入别硬编码进工具源码。如果你还没建 Key去 API Keys 页面生成一个权限按最小化给# API Keys 管理入口 # https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite这一步做完后面所有query()调用、工具调用产生的模型请求都会经过 TaoToken。工具本身的执行不依赖它但 Agent 的“大脑”走这条通道。3. 可复制配置settings.json 骨架与工具注册链路先给一份可以直接抄的settings.json骨架。它放在项目根目录的.claude/settings.json或者用户级的~/.claude/settings.json。进程内 MCP Server 不需要在这里注册工具但环境变量、权限模式、允许的工具白名单要在这里声明。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的统一Key }, permissions: { allow: [ mcp__internal-tools__query_knowledge_base, mcp__database-tools__list_database_tables, mcp__database-tools__query_sqlite ], deny: [ Bash(rm:*), Bash(curl:*internal.company.com*) ] }, mcpServers: {} }几个关键点。env里的两个变量让 SDK 走 TaoToken 通道。permissions.allow里的工具名格式是mcp__server名__工具名server 名就是你在createSdkMcpServer({ name: internal-tools })里写的那个。deny用来兜底比如禁止 Agent 直接 curl 内网地址——既然有了专属工具就别让它绕路。然后是工具注册链路分三步定义工具、创建 Server、挂载到query()。// tools/knowledge-base.ts import { tool } from anthropic-ai/claude-code import { z } from zod export const queryKnowledgeBase tool( query_knowledge_base, 查询公司内部知识库返回与问题相关的文档片段。适用于回答公司产品问题、查找技术规范、了解内部流程。, { question: z.string().describe(要查询的问题用自然语言描述), max_results: z .number() .int() .min(1) .max(10) .optional() .default(3) .describe(返回结果数量默认 3 条), }, async ({ question, max_results 3 }) { const response await fetch(https://internal.company.com/api/search, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.KNOWLEDGE_BASE_TOKEN}, }, body: JSON.stringify({ query: question, top_k: max_results }), }) if (!response.ok) { return { content: [ { type: text, text: 查询失败HTTP ${response.status} }, ], } } const data await response.json() const formatted data.results .map( (doc: { title: string; content: string; url: string }, i: number) --- 文档 ${i 1}${doc.title} ---\n${doc.content}\n来源${doc.url}, ) .join(\n\n) return { content: [{ type: text, text: formatted || 未找到相关文档 }], } }, )// agent.ts import { createSdkMcpServer, query } from anthropic-ai/claude-code import { queryKnowledgeBase } from ./tools/knowledge-base.js const toolServer createSdkMcpServer({ name: internal-tools, tools: [queryKnowledgeBase], }) async function askWithKnowledgeBase(question: string) { const response query({ prompt: question, options: { cwd: process.cwd(), mcpServers: { internal-tools: toolServer }, }, }) for await (const message of response) { if (message.type assistant) { const blocks message.message?.content ?? [] for (const block of blocks) { if (block.type text) process.stdout.write(block.text) if (block.type tool_use block.name query_knowledge_base) { console.log(\n[知识库查询] ${block.input?.question}) } } } if (message.type result) break } } await askWithKnowledgeBase(我们的 API 网关限速规则是怎样的)链路清晰了tool()定义单个工具createSdkMcpServer()把工具打包成 Serverquery()的mcpServers把 Server 挂进 Agent。Claude 在推理时看到工具名和描述自己决定调不调、怎么填参数。4. 验证请求一次端到端跑通与成功结果配置写完跑一次验证。先装依赖再执行。bun add anthropic-ai/claude-code zod bun run agent.ts预期输出分两段。第一段是 Claude 决定调用工具你会看到[知识库查询] API 网关限速规则第二段是它拿到文档片段后基于内容回答而不是靠训练数据猜。如果知识库返回了限速规则文档回答里会带上具体数值和来源链接。再验证一个更复杂的场景本地 SQLite 查询工具。定义两个工具一个列表结构一个执行 SELECT。// tools/sqlite-query.ts import { tool } from anthropic-ai/claude-code import { z } from zod import Database from better-sqlite3 const DB_PATH process.env.DB_PATH ?? ./data.db export const querySQLite tool( query_sqlite, 执行 SQLite 查询语句返回结果。只支持 SELECT不支持写操作。适用于数据分析场景。, { sql: z.string().describe(要执行的 SQL 查询语句仅支持 SELECT), limit: z.number().int().min(1).max(1000).optional().default(50) .describe(最多返回行数默认 50), }, async ({ sql, limit 50 }) { const normalized sql.trim().toUpperCase() if (!normalized.startsWith(SELECT)) { return { content: [{ type: text, text: 拒绝执行只允许 SELECT 语句。 }], } } try { const db new Database(DB_PATH, { readonly: true }) const rows db.prepare(${sql} LIMIT ${limit}).all() db.close() if (rows.length 0) { return { content: [{ type: text, text: 查询返回 0 条记录 }] } } const columns Object.keys(rows[0] as object) const header | ${columns.join( | )} | const separator | ${columns.map(() ---).join( | )} | const dataRows rows .map( (row) | ${columns .map((col) String((row as Recordstring, unknown)[col] ?? )) .join( | )} |, ) .join(\n) return { content: [ { type: text, text: 查询到 ${rows.length} 条记录\n\n${header}\n${separator}\n${dataRows}, }, ], } } catch (e) { return { content: [ { type: text, text: 查询出错${e instanceof Error ? e.message : String(e)} }, ], } } }, )注册进 Server 后用一句自然语言触发const dbToolServer createSdkMcpServer({ name: database-tools, tools: [listTables, querySQLite], }) await analyzeData(最近 30 天销售额最高的产品是哪些)成功结果的特征Claude 先调list_database_tables摸清表结构再调query_sqlite执行查询最后用中文解释结果。整个过程你不需要告诉它表名和字段名它自己看结构决定。5. 本篇常见错排查工具没被调用Claude 直接回答。九成是描述写得太含糊。query_knowledge_base的描述里要写清“什么时候用”比如“回答公司产品问题、查找技术规范时使用”。Claude 靠描述判断是否触发描述越具体命中率越高。参数填错或漏填。检查 Zod schema 的.describe()。Claude 靠这些描述理解每个参数的含义不写 describe 它就可能把max_results填成字符串。可选参数记得.optional().default()。报错mcpServers未识别。确认createSdkMcpServer的返回对象直接传给options.mcpServers键名和permissions.allow里的 server 名一致。进程内 Server 不需要写进settings.json的mcpServers字段。工具执行抛异常导致整个调用链崩溃。工具内部用 try/catch 包住失败时返回{ content: [{ type: text, text: 失败原因 }] }不要 throw。Claude 看到错误信息可以决定重试或换参数比未捕获异常好处理。权限被拒。如果开了permissionMode: default工具调用会弹确认。批量场景可以设bypassPermissions但生产环境建议用allow白名单精确放行别全局放开。模型请求 401 或超时。检查ANTHROPIC_BASE_URL是否写成https://taotoken.net/api不带斜杠后缀Key 是否有效。可以先用模型对话页面单独验证通道是否通# 模型对话验证入口 # https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewriteSQLite 工具报readonly错误。new Database(DB_PATH, { readonly: true })要求文件存在。先确认DB_PATH指向真实文件路径用绝对路径更稳。6. 工具设计原则与下一步工具名和描述是写给 Claude 看的不是写给人看的。get_data这种名字等于没写query_user_activity_in_last_n_days才能让模型秒懂。一个工具只做一件事“列表结构”和“执行查询”拆成两个Claude 自己会排调用顺序给它更细的控制权。返回格式优先 markdown 表格或有结构的文本别直接甩原始 JSON。工具结果会进消息历史格式清晰既省上下文又提升模型理解效率。安全边界必须在工具内部守querySQLite里的 SELECT-only 检查是工具自己的底线不能指望模型不乱写 SQL。多个工具协作时Claude 会自行决定顺序。代码质量分析 Agent 里analyze_package_json先摸项目元信息audit_dependencies再查漏洞最后综合出建议——你只需要在 prompt 里说清目标调度交给它。学完这篇你应该能用tool()定义带 Zod schema 的自定义工具用createSdkMcpServer()注册进 Agent用settings.json管好环境变量和权限白名单并让多个工具协作完成复杂任务。下一篇讲独立 MCP Server——把工具从进程内搬到进程外注册到settings.json让所有claude命令跨项目复用。如果你准备长期跑编码 Agent可以顺带了解 Coding Plan 的额度方案# Coding Plan 入口 # https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite接入文档在 doc 页面遇到通道配置问题先查那里# 接入文档 # https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite
返回列表