深入 Claude Code 源码(七):动手实践——用 SDK 跑起第一个 Agent

前六篇文章,我们把 Claude Code 的内部机制拆得相当透彻了:启动层、查询引擎、工具系统、上下文管理、MCP 协议、多智能体协调------每一块都知道是什么、为什么、怎么工作。

但有一件事情还没做------动手跑起来

光看懂源码,和真正能基于它做出东西,中间隔着一段实践的路。从本篇开始,我们把重心从「读懂」转向「做出来」,带大家一步一步完成从零到一个可用 Agent 的全过程。


一、两种调用方式:先把路选好

在动手之前,有一个问题值得先想清楚:做 Agent 有哪些路可以走?

基于我们对 Claude Code 源码的理解,答案是------两条路,复杂度不同,适合场景不同

路线 A:Shell 脚本调用(claude -p

最简单的方式。Claude Code 安装好之后,在任何脚本里直接调用 claude -p "你的任务" 就能拿到输出。适合快速验证想法、集成到 CI/CD 流水线、写不超过几十行的自动化脚本。

路线 B:Node.js/Bun SDK(@anthropic-ai/claude-code

稍微复杂一些,但能力强大得多。可以拿到完整的流式消息流、自定义工具、控制权限模式、处理多轮对话......回顾第二篇讲到的 QueryEngine,这条路就是直接使用它。适合要做产品级别的 Agent、需要接入自有系统的场景。

本篇两条路都带大家走一遍,让大家根据实际需要选择。


二、路线 A:三行代码跑起来

前置准备:确保已经安装了 Claude Code 并完成了登录。

bash 复制代码
# 安装(如果还没有)
npm install -g @anthropic-ai/claude-code

# 验证安装
claude --version

最简单的 Agent 调用

bash 复制代码
claude -p "列出当前目录下所有 TypeScript 文件,并统计每个文件的行数"

可以看到 Claude 会自动调用 Bash 工具执行 findwc -l,完成后打印出结果。整个过程不需要一行业务代码。

在 Shell 脚本里使用

bash 复制代码
#!/usr/bin/env bash
# 一个简单的代码分析脚本:analyze.sh

PROJECT_DIR=$1

if [ -z "$PROJECT_DIR" ]; then
  echo "用法: ./analyze.sh <项目目录>"
  exit 1
fi

echo "正在分析 $PROJECT_DIR ..."

claude -p "
  请分析 $PROJECT_DIR 目录下的代码,完成以下任务:
  1. 统计各类型文件的数量(.ts、.tsx、.js 等)
  2. 找出代码量最大的 5 个文件
  3. 识别项目使用的主要依赖(看 package.json)
  4. 给出一段 3-5 句话的项目概述
  用中文输出结果。
" --cwd "$PROJECT_DIR"

运行方式:

bash 复制代码
chmod +x analyze.sh
./analyze.sh ~/my-project

这里需要注意的是,-p 模式下 Claude 默认在当前目录下工作,用 --cwd 参数可以指定工作目录。

在 CI/CD 里使用

yaml 复制代码
# .github/workflows/ai-review.yml
name: AI Code Review

on: [pull_request]

jobs:
  ai-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: 安装 Claude Code
        run: 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-format=json > review.json
          
          # 把结果发到 PR 评论(可用 gh 命令)
          cat review.json

--output-format=json 参数让输出变成结构化的 JSON,方便后续处理。


三、路线 B:用 SDK 构建可控的 Agent

当需要更细粒度的控制时,就要用 SDK 了。

安装依赖

bash 复制代码
# 在你的 Node.js 或 Bun 项目里
npm install @anthropic-ai/claude-code
# 或者
bun add @anthropic-ai/claude-code

最简单的 SDK 调用

typescript 复制代码
// agent.ts
import { query } from '@anthropic-ai/claude-code'

async function runAgent() {
  const response = query({
    prompt: '分析当前目录的 package.json,告诉我这个项目用了哪些主要框架',
    options: {
      cwd: process.cwd(),
    },
  })

  // for await 消费流式消息
  for await (const message of response) {
    // 打印 Claude 的文字回复(流式,逐块输出)
    if (message.type === 'assistant') {
      const content = message.message?.content
      if (Array.isArray(content)) {
        for (const block of content) {
          if (block.type === 'text') {
            process.stdout.write(block.text)
          }
        }
      }
    }

    // 任务结束
    if (message.type === 'result') {
      console.log('\n\n--- 任务完成 ---')
      console.log(`耗时: ${message.duration_ms}ms`)
      console.log(`花费: $${message.total_cost_usd?.toFixed(4)}`)
      break
    }
  }
}

runAgent()

运行:

bash 复制代码
bun run agent.ts
# 或
npx tsx agent.ts

理解消息流

回顾第二篇讲的 AsyncGeneratorquery() 返回的就是一个消息流。消息有不同的 type,大家需要知道几个最关键的:

message.type 含义 什么时候出现
assistant Claude 的文字或工具调用 每次 Claude 有输出
user 工具执行结果(tool_result) Claude 调用工具后
result 整轮任务结束的汇总 最后一条消息
system 系统级事件(如压缩、错误重试) 按需出现

处理消息流的完整代码模板:

typescript 复制代码
import { query } from '@anthropic-ai/claude-code'

async function runAgent(prompt: string) {
  const response = query({
    prompt,
    options: { cwd: process.cwd() },
  })

  for await (const message of response) {
    switch (message.type) {
      case 'assistant': {
        // 打印文字内容
        const blocks = message.message?.content ?? []
        for (const block of blocks) {
          if (block.type === 'text' && block.text) {
            process.stdout.write(block.text)
          }
          // tool_use 块:Claude 决定调用某个工具
          if (block.type === 'tool_use') {
            console.log(`\n[工具调用] ${block.name}(${JSON.stringify(block.input)})`)
          }
        }
        break
      }

      case 'user': {
        // 工具执行结果
        const results = message.message?.content ?? []
        for (const block of results) {
          if (block.type === 'tool_result') {
            console.log(`[工具结果] ${String(block.content).slice(0, 100)}...`)
          }
        }
        break
      }

      case 'result': {
        // 任务结束
        if (message.subtype === 'success') {
          console.log('\n✓ 任务成功完成')
        } else {
          console.log(`\n✗ 任务失败: ${message.subtype}`)
          if (message.errors) {
            console.log('错误信息:', message.errors)
          }
        }
        return message  // 返回最终结果
      }

      case 'system': {
        if (message.subtype === 'api_retry') {
          console.log(`[重试] 第 ${message.attempt} 次重试...`)
        }
        break
      }
    }
  }
}

// 使用
await runAgent('列出所有 .ts 文件并找出最长的函数')

四、第一个有意义的 Agent:代码变更摘要器

理论讲完了,我们来做一个真实有用的东西:一个自动总结 git diff 的 Agent

在代码 review 场景里,最耗时的部分之一是「理解这次提交改了什么」。让 Agent 做这件事,是一个非常合适的起点。

typescript 复制代码
// summarize-diff.ts
import { query } from '@anthropic-ai/claude-code'
import { execSync } from 'child_process'

async function summarizeGitDiff(baseBranch = 'main') {
  // 先拿到 diff 内容
  let diff: string
  try {
    diff = execSync(`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,避免超出 context
  const MAX_DIFF_CHARS = 50_000
  const truncated = diff.length > MAX_DIFF_CHARS
  const diffToAnalyze = truncated
    ? diff.slice(0, MAX_DIFF_CHARS) + '\n\n[... diff 内容过长,已截断]'
    : diff

  console.log(`分析 ${diff.length} 字符的 diff,请稍候...\n`)

  const prompt = `
    请分析以下 git diff,用中文输出一份结构化的变更摘要:

    要求:
    1. 用一句话概括本次变更的核心目的
    2. 列出主要变更点(3-7 条,按重要程度排序)
    3. 标注需要重点关注的风险点(如果有)
    4. 格式要简洁,方便直接粘贴到 PR 描述里

    diff 内容:
    \`\`\`
    ${diffToAnalyze}
    \`\`\`
  `

  const response = query({
    prompt,
    options: {
      cwd: process.cwd(),
      // 这个任务只需要分析文本,不需要工具
      allowedTools: [],
    },
  })

  process.stdout.write('📋 变更摘要:\n\n')

  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 (message.type === 'result') {
      console.log(`\n\n[耗时 ${message.duration_ms}ms,花费 $${message.total_cost_usd?.toFixed(5)}]`)
      break
    }
  }
}

// 从命令行参数读取 base branch,默认 main
const baseBranch = process.argv[2] ?? 'main'
await summarizeGitDiff(baseBranch)

运行效果:

bash 复制代码
bun run summarize-diff.ts
# 或者指定 base branch
bun 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 选项来控制:

typescript 复制代码
const response = query({
  prompt: '...',
  options: {
    cwd: process.cwd(),
    // 完全绕过权限确认(适合受信任的自动化脚本)
    permissionMode: 'bypassPermissions',
  },
})

这里需要特别说明------bypassPermissions 意味着 Claude 可以不经确认地读写任何文件、执行任何命令。在本地开发脚本里用是可以的,但在接受外部输入(比如读取用户提交的代码来处理)的场景里,一定要结合其他安全措施,不要无条件开启。

三种权限模式的选择原则:

  • default:交互式场景,危险操作需要确认
  • acceptEdits:半自动化场景,接受文件改动但 Bash 仍需确认
  • bypassPermissions:完全自动化的受信任脚本

六、多轮对话

query() 的单次调用相当于「一个用户消息 → Claude 执行到结束」。如果需要多轮对话,可以用 --resume 恢复会话,或者在 SDK 里使用 sessionId

typescript 复制代码
import { query } from '@anthropic-ai/claude-code'

// 第一轮
let sessionId: string | undefined

const response1 = query({
  prompt: '帮我找出项目里所有的 TODO 注释',
  options: { cwd: process.cwd() },
})

for await (const message of response1) {
  if (message.type === 'result') {
    sessionId = message.session_id   // 保存 session id
    console.log('找到的 TODO:', message.result)
    break
  }
}

// 第二轮:在同一个 session 里继续
if (sessionId) {
  const response2 = query({
    prompt: '针对刚才找到的这些 TODO,哪些是高优先级的?',
    options: {
      cwd: process.cwd(),
      resume: sessionId,   // 恢复上一轮的上下文
    },
  })

  for await (const message of response2) {
    if (message.type === 'assistant') {
      const blocks = message.message?.content ?? []
      for (const block of blocks) {
        if (block.type === 'text') process.stdout.write(block.text)
      }
    }
    if (message.type === 'result') break
  }
}

这就像两位同事之间的接力对话------第二位同事接手时,完全知道第一轮聊了什么,不需要从头解释背景。


学习完本篇,大家应该能做到:

  1. claude -p 在脚本和 CI/CD 里快速集成 AI 能力
  2. 用 SDK 的 query() 函数获取完整的流式消息流
  3. 处理 assistantuserresult 等不同类型的消息
  4. 控制权限模式,让 Agent 在自动化场景里无需交互确认
  5. session_id 实现多轮对话

如果第一次写这些代码遇到报错,先不要纠结。先确认安装正确、API key 设置正确、在 git 仓库目录下运行,大部分问题都出在环境配置上,和代码逻辑无关。

接下来,进入第八篇------自定义工具,给 Agent 加上只有你才有的专属能力。