前六篇文章我们把 Claude Code 的内部机制拆得相当透彻了启动层、查询引擎、工具系统、上下文管理、MCP 协议、多智能体协调——每一块都知道是什么、为什么、怎么工作。但有一件事情还没做——动手跑起来。光看懂源码和真正能基于它做出东西中间隔着一段实践的路。从本篇开始我们把重心从「读懂」转向「做出来」带大家一步一步完成从零到一个可用 Agent 的全过程。一、两种调用方式先把路选好在动手之前有一个问题值得先想清楚做 Agent 有哪些路可以走基于我们对 Claude Code 源码的理解答案是——两条路复杂度不同适合场景不同。路线 AShell 脚本调用claude -p最简单的方式。Claude Code 安装好之后在任何脚本里直接调用claude -p 你的任务就能拿到输出。适合快速验证想法、集成到 CI/CD 流水线、写不超过几十行的自动化脚本。路线 BNode.js/Bun SDKanthropic-ai/claude-code稍微复杂一些但能力强大得多。可以拿到完整的流式消息流、自定义工具、控制权限模式、处理多轮对话……回顾第二篇讲到的QueryEngine这条路就是直接使用它。适合要做产品级别的 Agent、需要接入自有系统的场景。本篇两条路都带大家走一遍让大家根据实际需要选择。二、路线 A三行代码跑起来前置准备确保已经安装了 Claude Code 并完成了登录。# 安装如果还没有npminstall-ganthropic-ai/claude-code# 验证安装claude--version最简单的 Agent 调用claude-p列出当前目录下所有 TypeScript 文件并统计每个文件的行数可以看到 Claude 会自动调用Bash工具执行find和wc -l完成后打印出结果。整个过程不需要一行业务代码。在 Shell 脚本里使用#!/usr/bin/env bash# 一个简单的代码分析脚本analyze.shPROJECT_DIR$1if[-z$PROJECT_DIR];thenecho用法: ./analyze.sh 项目目录exit1fiecho正在分析$PROJECT_DIR...claude-p 请分析$PROJECT_DIR目录下的代码完成以下任务 1. 统计各类型文件的数量.ts、.tsx、.js 等 2. 找出代码量最大的 5 个文件 3. 识别项目使用的主要依赖看 package.json 4. 给出一段 3-5 句话的项目概述 用中文输出结果。 --cwd$PROJECT_DIR运行方式chmodx analyze.sh ./analyze.sh ~/my-project这里需要注意的是-p模式下 Claude 默认在当前目录下工作用--cwd参数可以指定工作目录。在 CI/CD 里使用# .github/workflows/ai-review.ymlname:AI Code Reviewon:[pull_request]jobs:ai-review:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4-name:安装 Claude Coderun:npm install-g anthropic-ai/claude-code-name:运行 AI 代码审查env:ANTHROPIC_API_KEY:${{secrets.ANTHROPIC_API_KEY}}run:|# 拿到本次 PR 的变更文件列表 CHANGED_FILES$(git diff --name-only origin/main...HEAD)claude-p 请对以下变更文件做简短的代码审查重点关注-潜在的 Bug-安全风险-明显的性能问题 变更文件$CHANGED_FILES --output-formatjsonreview.json# 把结果发到 PR 评论可用 gh 命令cat review.json--output-formatjson参数让输出变成结构化的 JSON方便后续处理。三、路线 B用 SDK 构建可控的 Agent当需要更细粒度的控制时就要用 SDK 了。安装依赖# 在你的 Node.js 或 Bun 项目里npminstallanthropic-ai/claude-code# 或者bunaddanthropic-ai/claude-code最简单的 SDK 调用// agent.tsimport{query}fromanthropic-ai/claude-codeasyncfunctionrunAgent(){constresponsequery({prompt:分析当前目录的 package.json告诉我这个项目用了哪些主要框架,options:{cwd:process.cwd(),},})// for await 消费流式消息forawait(constmessageofresponse){// 打印 Claude 的文字回复流式逐块输出if(message.typeassistant){constcontentmessage.message?.contentif(Array.isArray(content)){for(constblockofcontent){if(block.typetext){process.stdout.write(block.text)}}}}// 任务结束if(message.typeresult){console.log(\n\n--- 任务完成 ---)console.log(耗时:${message.duration_ms}ms)console.log(花费: $${message.total_cost_usd?.toFixed(4)})break}}}runAgent()运行bun run agent.ts# 或npx tsx agent.ts理解消息流回顾第二篇讲的AsyncGeneratorquery()返回的就是一个消息流。消息有不同的type大家需要知道几个最关键的message.type含义什么时候出现assistantClaude 的文字或工具调用每次 Claude 有输出user工具执行结果tool_resultClaude 调用工具后result整轮任务结束的汇总最后一条消息system系统级事件如压缩、错误重试按需出现处理消息流的完整代码模板import{query}fromanthropic-ai/claude-codeasyncfunctionrunAgent(prompt:string){constresponsequery({prompt,options:{cwd:process.cwd()},})forawait(constmessageofresponse){switch(message.type){caseassistant:{// 打印文字内容constblocksmessage.message?.content??[]for(constblockofblocks){if(block.typetextblock.text){process.stdout.write(block.text)}// tool_use 块Claude 决定调用某个工具if(block.typetool_use){console.log(\n[工具调用]${block.name}(${JSON.stringify(block.input)}))}}break}caseuser:{// 工具执行结果constresultsmessage.message?.content??[]for(constblockofresults){if(block.typetool_result){console.log([工具结果]${String(block.content).slice(0,100)}...)}}break}caseresult:{// 任务结束if(message.subtypesuccess){console.log(\n✓ 任务成功完成)}else{console.log(\n✗ 任务失败:${message.subtype})if(message.errors){console.log(错误信息:,message.errors)}}returnmessage// 返回最终结果}casesystem:{if(message.subtypeapi_retry){console.log([重试] 第${message.attempt}次重试...)}break}}}}// 使用awaitrunAgent(列出所有 .ts 文件并找出最长的函数)四、第一个有意义的 Agent代码变更摘要器理论讲完了我们来做一个真实有用的东西一个自动总结 git diff 的 Agent。在代码 review 场景里最耗时的部分之一是「理解这次提交改了什么」。让 Agent 做这件事是一个非常合适的起点。// summarize-diff.tsimport{query}fromanthropic-ai/claude-codeimport{execSync}fromchild_processasyncfunctionsummarizeGitDiff(baseBranchmain){// 先拿到 diff 内容letdiff:stringtry{diffexecSync(git diff${baseBranch}...HEAD,{encoding:utf-8,maxBuffer:10*1024*1024,// 10MB 上限})}catch(e){console.error(无法获取 git diff请确保在 git 仓库目录下运行)process.exit(1)}if(!diff.trim()){console.log(和主分支没有差异)return}// 截断过长的 diff避免超出 contextconstMAX_DIFF_CHARS50_000consttruncateddiff.lengthMAX_DIFF_CHARSconstdiffToAnalyzetruncated?diff.slice(0,MAX_DIFF_CHARS)\n\n[... diff 内容过长已截断]:diffconsole.log(分析${diff.length}字符的 diff请稍候...\n)constprompt请分析以下 git diff用中文输出一份结构化的变更摘要 要求 1. 用一句话概括本次变更的核心目的 2. 列出主要变更点3-7 条按重要程度排序 3. 标注需要重点关注的风险点如果有 4. 格式要简洁方便直接粘贴到 PR 描述里 diff 内容 \\\${diffToAnalyze}\\\constresponsequery({prompt,options:{cwd:process.cwd(),// 这个任务只需要分析文本不需要工具allowedTools:[],},})process.stdout.write( 变更摘要\n\n)forawait(constmessageofresponse){if(message.typeassistant){constblocksmessage.message?.content??[]for(constblockofblocks){if(block.typetext){process.stdout.write(block.text)}}}if(message.typeresult){console.log(\n\n[耗时${message.duration_ms}ms花费 $${message.total_cost_usd?.toFixed(5)}])break}}}// 从命令行参数读取 base branch默认 mainconstbaseBranchprocess.argv[2]??mainawaitsummarizeGitDiff(baseBranch)运行效果bun run summarize-diff.ts# 或者指定 base branchbun run summarize-diff.ts develop输出大致如下分析 8432 字符的 diff请稍候... 变更摘要 **核心目的**重构工具权限检查模块将散落在各工具中的权限逻辑收敛到统一的 PermissionManager 类。 **主要变更** - 新增 PermissionManager 类src/utils/permissions/manager.ts统一管理工具权限规则 - BashTool 移除内联权限逻辑改为调用 PermissionManager.canUse() - FileEditTool 同步更新使用新的权限接口 - 更新 QueryEngine.ts 中的 wrappedCanUseTool适配新接口 - 补充了 PermissionManager 的单元测试tests/permissions/manager.test.ts **风险点** - 旧的 bashToolHasPermission 函数仍保留backward compat shim可以在下个版本删除 - 需要验证 bypassPermissions 模式下的行为是否与之前一致 [耗时 4821ms花费 $0.00312]这个 Agent 的实用价值很高——每次提交 PR 之前跑一次自动生成 PR 描述的第一稿比手写快多了。五、控制权限模式大家可能注意到默认情况下 Agent 运行时Claude 调用工具仍然会弹出确认框。在自动化场景里这显然不合适。可以通过permissionMode选项来控制constresponsequery({prompt:...,options:{cwd:process.cwd(),// 完全绕过权限确认适合受信任的自动化脚本permissionMode:bypassPermissions,},})这里需要特别说明——bypassPermissions意味着 Claude 可以不经确认地读写任何文件、执行任何命令。在本地开发脚本里用是可以的但在接受外部输入比如读取用户提交的代码来处理的场景里一定要结合其他安全措施不要无条件开启。三种权限模式的选择原则default交互式场景危险操作需要确认acceptEdits半自动化场景接受文件改动但 Bash 仍需确认bypassPermissions完全自动化的受信任脚本六、多轮对话query()的单次调用相当于「一个用户消息 → Claude 执行到结束」。如果需要多轮对话可以用--resume恢复会话或者在 SDK 里使用sessionIdimport{query}fromanthropic-ai/claude-code// 第一轮letsessionId:string|undefinedconstresponse1query({prompt:帮我找出项目里所有的 TODO 注释,options:{cwd:process.cwd()},})forawait(constmessageofresponse1){if(message.typeresult){sessionIdmessage.session_id// 保存 session idconsole.log(找到的 TODO:,message.result)break}}// 第二轮在同一个 session 里继续if(sessionId){constresponse2query({prompt:针对刚才找到的这些 TODO哪些是高优先级的,options:{cwd:process.cwd(),resume:sessionId,// 恢复上一轮的上下文},})forawait(constmessageofresponse2){if(message.typeassistant){constblocksmessage.message?.content??[]for(constblockofblocks){if(block.typetext)process.stdout.write(block.text)}}if(message.typeresult)break}}这就像两位同事之间的接力对话——第二位同事接手时完全知道第一轮聊了什么不需要从头解释背景。学习完本篇大家应该能做到用claude -p在脚本和 CI/CD 里快速集成 AI 能力用 SDK 的query()函数获取完整的流式消息流处理assistant、user、result等不同类型的消息控制权限模式让 Agent 在自动化场景里无需交互确认用session_id实现多轮对话如果第一次写这些代码遇到报错先不要纠结。先确认安装正确、API key 设置正确、在 git 仓库目录下运行大部分问题都出在环境配置上和代码逻辑无关。接下来进入第八篇——自定义工具给 Agent 加上只有你才有的专属能力。
深入 Claude Code 源码(七):动手实践——用 SDK 跑起第一个 Agent
前六篇文章我们把 Claude Code 的内部机制拆得相当透彻了启动层、查询引擎、工具系统、上下文管理、MCP 协议、多智能体协调——每一块都知道是什么、为什么、怎么工作。但有一件事情还没做——动手跑起来。光看懂源码和真正能基于它做出东西中间隔着一段实践的路。从本篇开始我们把重心从「读懂」转向「做出来」带大家一步一步完成从零到一个可用 Agent 的全过程。一、两种调用方式先把路选好在动手之前有一个问题值得先想清楚做 Agent 有哪些路可以走基于我们对 Claude Code 源码的理解答案是——两条路复杂度不同适合场景不同。路线 AShell 脚本调用claude -p最简单的方式。Claude Code 安装好之后在任何脚本里直接调用claude -p 你的任务就能拿到输出。适合快速验证想法、集成到 CI/CD 流水线、写不超过几十行的自动化脚本。路线 BNode.js/Bun SDKanthropic-ai/claude-code稍微复杂一些但能力强大得多。可以拿到完整的流式消息流、自定义工具、控制权限模式、处理多轮对话……回顾第二篇讲到的QueryEngine这条路就是直接使用它。适合要做产品级别的 Agent、需要接入自有系统的场景。本篇两条路都带大家走一遍让大家根据实际需要选择。二、路线 A三行代码跑起来前置准备确保已经安装了 Claude Code 并完成了登录。# 安装如果还没有npminstall-ganthropic-ai/claude-code# 验证安装claude--version最简单的 Agent 调用claude-p列出当前目录下所有 TypeScript 文件并统计每个文件的行数可以看到 Claude 会自动调用Bash工具执行find和wc -l完成后打印出结果。整个过程不需要一行业务代码。在 Shell 脚本里使用#!/usr/bin/env bash# 一个简单的代码分析脚本analyze.shPROJECT_DIR$1if[-z$PROJECT_DIR];thenecho用法: ./analyze.sh 项目目录exit1fiecho正在分析$PROJECT_DIR...claude-p 请分析$PROJECT_DIR目录下的代码完成以下任务 1. 统计各类型文件的数量.ts、.tsx、.js 等 2. 找出代码量最大的 5 个文件 3. 识别项目使用的主要依赖看 package.json 4. 给出一段 3-5 句话的项目概述 用中文输出结果。 --cwd$PROJECT_DIR运行方式chmodx analyze.sh ./analyze.sh ~/my-project这里需要注意的是-p模式下 Claude 默认在当前目录下工作用--cwd参数可以指定工作目录。在 CI/CD 里使用# .github/workflows/ai-review.ymlname:AI Code Reviewon:[pull_request]jobs:ai-review:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4-name:安装 Claude Coderun:npm install-g anthropic-ai/claude-code-name:运行 AI 代码审查env:ANTHROPIC_API_KEY:${{secrets.ANTHROPIC_API_KEY}}run:|# 拿到本次 PR 的变更文件列表 CHANGED_FILES$(git diff --name-only origin/main...HEAD)claude-p 请对以下变更文件做简短的代码审查重点关注-潜在的 Bug-安全风险-明显的性能问题 变更文件$CHANGED_FILES --output-formatjsonreview.json# 把结果发到 PR 评论可用 gh 命令cat review.json--output-formatjson参数让输出变成结构化的 JSON方便后续处理。三、路线 B用 SDK 构建可控的 Agent当需要更细粒度的控制时就要用 SDK 了。安装依赖# 在你的 Node.js 或 Bun 项目里npminstallanthropic-ai/claude-code# 或者bunaddanthropic-ai/claude-code最简单的 SDK 调用// agent.tsimport{query}fromanthropic-ai/claude-codeasyncfunctionrunAgent(){constresponsequery({prompt:分析当前目录的 package.json告诉我这个项目用了哪些主要框架,options:{cwd:process.cwd(),},})// for await 消费流式消息forawait(constmessageofresponse){// 打印 Claude 的文字回复流式逐块输出if(message.typeassistant){constcontentmessage.message?.contentif(Array.isArray(content)){for(constblockofcontent){if(block.typetext){process.stdout.write(block.text)}}}}// 任务结束if(message.typeresult){console.log(\n\n--- 任务完成 ---)console.log(耗时:${message.duration_ms}ms)console.log(花费: $${message.total_cost_usd?.toFixed(4)})break}}}runAgent()运行bun run agent.ts# 或npx tsx agent.ts理解消息流回顾第二篇讲的AsyncGeneratorquery()返回的就是一个消息流。消息有不同的type大家需要知道几个最关键的message.type含义什么时候出现assistantClaude 的文字或工具调用每次 Claude 有输出user工具执行结果tool_resultClaude 调用工具后result整轮任务结束的汇总最后一条消息system系统级事件如压缩、错误重试按需出现处理消息流的完整代码模板import{query}fromanthropic-ai/claude-codeasyncfunctionrunAgent(prompt:string){constresponsequery({prompt,options:{cwd:process.cwd()},})forawait(constmessageofresponse){switch(message.type){caseassistant:{// 打印文字内容constblocksmessage.message?.content??[]for(constblockofblocks){if(block.typetextblock.text){process.stdout.write(block.text)}// tool_use 块Claude 决定调用某个工具if(block.typetool_use){console.log(\n[工具调用]${block.name}(${JSON.stringify(block.input)}))}}break}caseuser:{// 工具执行结果constresultsmessage.message?.content??[]for(constblockofresults){if(block.typetool_result){console.log([工具结果]${String(block.content).slice(0,100)}...)}}break}caseresult:{// 任务结束if(message.subtypesuccess){console.log(\n✓ 任务成功完成)}else{console.log(\n✗ 任务失败:${message.subtype})if(message.errors){console.log(错误信息:,message.errors)}}returnmessage// 返回最终结果}casesystem:{if(message.subtypeapi_retry){console.log([重试] 第${message.attempt}次重试...)}break}}}}// 使用awaitrunAgent(列出所有 .ts 文件并找出最长的函数)四、第一个有意义的 Agent代码变更摘要器理论讲完了我们来做一个真实有用的东西一个自动总结 git diff 的 Agent。在代码 review 场景里最耗时的部分之一是「理解这次提交改了什么」。让 Agent 做这件事是一个非常合适的起点。// summarize-diff.tsimport{query}fromanthropic-ai/claude-codeimport{execSync}fromchild_processasyncfunctionsummarizeGitDiff(baseBranchmain){// 先拿到 diff 内容letdiff:stringtry{diffexecSync(git diff${baseBranch}...HEAD,{encoding:utf-8,maxBuffer:10*1024*1024,// 10MB 上限})}catch(e){console.error(无法获取 git diff请确保在 git 仓库目录下运行)process.exit(1)}if(!diff.trim()){console.log(和主分支没有差异)return}// 截断过长的 diff避免超出 contextconstMAX_DIFF_CHARS50_000consttruncateddiff.lengthMAX_DIFF_CHARSconstdiffToAnalyzetruncated?diff.slice(0,MAX_DIFF_CHARS)\n\n[... diff 内容过长已截断]:diffconsole.log(分析${diff.length}字符的 diff请稍候...\n)constprompt请分析以下 git diff用中文输出一份结构化的变更摘要 要求 1. 用一句话概括本次变更的核心目的 2. 列出主要变更点3-7 条按重要程度排序 3. 标注需要重点关注的风险点如果有 4. 格式要简洁方便直接粘贴到 PR 描述里 diff 内容 \\\${diffToAnalyze}\\\constresponsequery({prompt,options:{cwd:process.cwd(),// 这个任务只需要分析文本不需要工具allowedTools:[],},})process.stdout.write( 变更摘要\n\n)forawait(constmessageofresponse){if(message.typeassistant){constblocksmessage.message?.content??[]for(constblockofblocks){if(block.typetext){process.stdout.write(block.text)}}}if(message.typeresult){console.log(\n\n[耗时${message.duration_ms}ms花费 $${message.total_cost_usd?.toFixed(5)}])break}}}// 从命令行参数读取 base branch默认 mainconstbaseBranchprocess.argv[2]??mainawaitsummarizeGitDiff(baseBranch)运行效果bun run summarize-diff.ts# 或者指定 base branchbun run summarize-diff.ts develop输出大致如下分析 8432 字符的 diff请稍候... 变更摘要 **核心目的**重构工具权限检查模块将散落在各工具中的权限逻辑收敛到统一的 PermissionManager 类。 **主要变更** - 新增 PermissionManager 类src/utils/permissions/manager.ts统一管理工具权限规则 - BashTool 移除内联权限逻辑改为调用 PermissionManager.canUse() - FileEditTool 同步更新使用新的权限接口 - 更新 QueryEngine.ts 中的 wrappedCanUseTool适配新接口 - 补充了 PermissionManager 的单元测试tests/permissions/manager.test.ts **风险点** - 旧的 bashToolHasPermission 函数仍保留backward compat shim可以在下个版本删除 - 需要验证 bypassPermissions 模式下的行为是否与之前一致 [耗时 4821ms花费 $0.00312]这个 Agent 的实用价值很高——每次提交 PR 之前跑一次自动生成 PR 描述的第一稿比手写快多了。五、控制权限模式大家可能注意到默认情况下 Agent 运行时Claude 调用工具仍然会弹出确认框。在自动化场景里这显然不合适。可以通过permissionMode选项来控制constresponsequery({prompt:...,options:{cwd:process.cwd(),// 完全绕过权限确认适合受信任的自动化脚本permissionMode:bypassPermissions,},})这里需要特别说明——bypassPermissions意味着 Claude 可以不经确认地读写任何文件、执行任何命令。在本地开发脚本里用是可以的但在接受外部输入比如读取用户提交的代码来处理的场景里一定要结合其他安全措施不要无条件开启。三种权限模式的选择原则default交互式场景危险操作需要确认acceptEdits半自动化场景接受文件改动但 Bash 仍需确认bypassPermissions完全自动化的受信任脚本六、多轮对话query()的单次调用相当于「一个用户消息 → Claude 执行到结束」。如果需要多轮对话可以用--resume恢复会话或者在 SDK 里使用sessionIdimport{query}fromanthropic-ai/claude-code// 第一轮letsessionId:string|undefinedconstresponse1query({prompt:帮我找出项目里所有的 TODO 注释,options:{cwd:process.cwd()},})forawait(constmessageofresponse1){if(message.typeresult){sessionIdmessage.session_id// 保存 session idconsole.log(找到的 TODO:,message.result)break}}// 第二轮在同一个 session 里继续if(sessionId){constresponse2query({prompt:针对刚才找到的这些 TODO哪些是高优先级的,options:{cwd:process.cwd(),resume:sessionId,// 恢复上一轮的上下文},})forawait(constmessageofresponse2){if(message.typeassistant){constblocksmessage.message?.content??[]for(constblockofblocks){if(block.typetext)process.stdout.write(block.text)}}if(message.typeresult)break}}这就像两位同事之间的接力对话——第二位同事接手时完全知道第一轮聊了什么不需要从头解释背景。学习完本篇大家应该能做到用claude -p在脚本和 CI/CD 里快速集成 AI 能力用 SDK 的query()函数获取完整的流式消息流处理assistant、user、result等不同类型的消息控制权限模式让 Agent 在自动化场景里无需交互确认用session_id实现多轮对话如果第一次写这些代码遇到报错先不要纠结。先确认安装正确、API key 设置正确、在 git 仓库目录下运行大部分问题都出在环境配置上和代码逻辑无关。接下来进入第八篇——自定义工具给 Agent 加上只有你才有的专属能力。