CodeBuddy 学习(8):Agent SDK

一、概述:SDK 是什么,解决什么问题

1.1 对话式 vs 编程式

对比维度 对话式(终端/IDE 内对话) 编程式(Agent SDK)
触发方式 手动输入指令 代码中调用 API
适用场景 本地开发、调试、探索 CI/CD 自动化、Web 后端、定时任务
交互前提 需要人在终端前 全自动,无需人工介入
输出方式 终端/面板直接显示 可编程解析(JSON 等)

1.2 原理讲解

Agent SDK 是一个 TypeScript SDK,让你在代码中调用 CodeBuddy——本质上是以编程方式启动 AI Agent

工作原理

  1. SDK 在 Node.js 进程中通过 @tencent-ai/agent-sdk 包导入。
  2. 调用 query() 时,SDK 启动一个 CodeBuddy CLI 子进程。
  3. 通过 stdin/stdout 与子进程通信——发送指令、接收结果。
  4. 结果以流式(AsyncGenerator)的方式返回,可逐条处理。
你的代码  →  query()  →  CodeBuddy CLI 子进程  →  AI 云端服务
    ↑                                                      |
    └──────────── 流式返回结果(stdout) ←──────────────────┘

1.3 环境准备

# 克隆示例仓库
git clone https://cnb.cool/codebuddy/agent-sdk-demos
cd agent-sdk-demos/profile-builder

# 安装依赖
npm install
npm install @tencent-ai/agent-sdk typescript @types/node tsx zod

# 设置 API Key(从 CodeBuddy 控制台获取)
# Windows (PowerShell):
$env:CODEBUDDY_API_KEY = "你的Key"

# macOS/Linux:
export CODEBUDDY_API_KEY="你的Key"

二、示例一:最简 query() 调用

2.1 原理

query() 是 SDK 的核心函数。每个 query() 调用是独立的——不会保留上一次调用的上下文(像每次都是新对话)。它返回一个 AsyncGenerator,你可以用 for await...of 逐条消费 AI 返回的消息。

四个关键参数

  • prompt:任务描述(对应终端中的自然语言指令)。
  • maxTurns:最大交互轮数(防止无限循环)。
  • model:使用的 AI 模型。
  • cwd:工作目录(AI 的文件操作范围)。

消息类型

  • system:系统通知(如会话创建、权限检查)。
  • assistant:AI 的回复(包含 text、tool_use 等 block)。
  • result:工具执行结果。

2.2 完整代码

// step1-basic.ts — 最简 SDK 调用示例
import { query } from '@tencent-ai/agent-sdk';

async function main() {
  console.log('开始调用 CodeBuddy Agent...\n');

  // 发起查询(返回 AsyncGenerator)
  const response = query({
    prompt: '请用 TypeScript 编写一个计算斐波那契数列的函数,包含 JSDoc 注释',
    options: {
      maxTurns: 3,                           // 最多 3 轮交互
      model: 'claude-sonnet-4-20250514',     // 使用的模型
      // cwd: './my-project',                // (可选)指定工作目录
    }
  });

  // 逐条消费流式结果
  for await (const message of response) {
    switch (message.type) {
      case 'system':
        // 系统消息:会话创建、权限检查等
        console.log(`[系统] ${message.subtype}: ${JSON.stringify(message)}`);
        break;

      case 'assistant':
        // AI 的回复
        for (const block of message.content) {
          if (block.type === 'text') {
            console.log(`[AI 回复]\n${block.text}\n`);
          } else if (block.type === 'tool_use') {
            console.log(`[工具调用] ${block.name}: ${JSON.stringify(block.input)}`);
          }
        }
        break;

      case 'result':
        // 工具执行结果(如文件写入完成)
        console.log(`[结果] ${message.subtype}`);
        break;
    }
  }

  console.log('查询完成。');
}

main().catch((err) => {
  console.error('调用失败:', err.message);
  process.exit(1);
});

运行

tsx step1-basic.ts

2.3 预期输出

开始调用 CodeBuddy Agent...

[系统] init: {"sessionId":"abc123...","cwd":"/current/dir"}
[AI 回复]
以下是用 TypeScript 编写的斐波那契数列计算函数:

```typescript
/**
 * 计算斐波那契数列的第 n 项
 * 使用迭代法实现,时间复杂度 O(n),空间复杂度 O(1)
 *
 * @param n - 要计算的项数(从 0 开始)
 * @returns 第 n 项的斐波那契数值
 * @throws {Error} 如果 n 为负数
 *
 * @example
 * fibonacci(0)  // => 0
 * fibonacci(1)  // => 1
 * fibonacci(10) // => 55
 */
function fibonacci(n: number): number {
  if (n < 0) throw new Error('n 不能为负数');
  if (n <= 1) return n;

  let prev = 0;
  let curr = 1;

  for (let i = 2; i <= n; i++) {
    const next = prev + curr;
    prev = curr;
    curr = next;
  }

  return curr;
}

查询完成。


### 2.4 调试过程

**场景一:`CODEBUDDY_API_KEY` 未设置**

现象:运行时报错 “Authentication failed” 或 “API key not found”。
原因:环境变量 CODEBUDDY_API_KEY 未设置。
排查:
echo $CODEBUDDY_API_KEY # macOS/Linux
echo $env:CODEBUDDY_API_KEY # Windows PowerShell
如果输出为空,说明未设置。
修复:

  • 从 CodeBuddy 控制台获取 API Key。
  • 设置环境变量后重新运行。
  • 或者在代码中显式传入(不推荐,有泄露风险)。

**场景二:`maxTurns` 设置过小导致任务中断**

现象:AI 的输出在开始写代码时突然中断,收到 “Max turns reached” 的系统消息。
原因:maxTurns 限制了 AI 可以执行的工具调用次数。
如果任务需要 Write + Read + Bash 三步,但 maxTurns 设为 1,就会中断。
排查:检查任务实际需要的工具调用次数,对比 maxTurns 设置。
修复:增大 maxTurns。一般建议:

  • 简单问答:3
  • 文件创建/修改:5
  • 复杂多步骤任务:10-15

**场景三:TypeScript 编译错误**

现象:tsx 运行时报类型错误。
原因:agent-sdk 的类型定义与 TypeScript 版本不兼容。
排查:

  1. 检查 tsconfig.json 中的 target 和 module 设置。
  2. 检查 @tencent-ai/agent-sdk 的版本。
    修复:
  3. 在 tsconfig.json 中设置 “skipLibCheck”: true。
  4. 或使用 tsx 运行时添加 --ignore-ts-errors 参数。

---

## 三、示例二:添加工具权限控制

### 3.1 原理

默认情况下,`query()` 允许 AI 使用所有可用工具。但你可以通过 `allowedTools` **白名单机制**限制 AI 只能使用指定工具——这是一种安全措施,防止 AI 意外执行危险操作。

**可用工具分类**:
- 文件操作:`Read`、`Write`、`Edit`
- 搜索:`Glob`、`Grep`、`WebSearch`
- 执行:`Bash`、`Task`
- 实用工具:`TodoWrite`、`WebFetch`

### 3.2 完整代码

```typescript
// step2-tools.ts — 带工具控制的 SDK 调用
import { query } from '@tencent-ai/agent-sdk';

async function analyzeProject() {
  const response = query({
    prompt: `读取 src/ 目录下所有 .ts 文件,列出每个文件的导出函数名,
             并将结果写入 analysis-report.md。`,
    options: {
      maxTurns: 8,
      cwd: './my-project',
      // 白名单:只允许 Read、Glob、Write 三种工具
      // AI 无法执行 Bash 命令,也无法搜索 Web
      allowedTools: ['Read', 'Glob', 'Write'],
      model: 'claude-sonnet-4-20250514',
    }
  });

  for await (const message of response) {
    if (message.type === 'assistant') {
      for (const block of message.content) {
        if (block.type === 'text') {
          console.log(`[AI] ${block.text.substring(0, 200)}...`);
        }
        if (block.type === 'tool_use') {
          console.log(`[工具] 使用 ${block.name}: ${JSON.stringify(block.input).substring(0, 100)}`);
        }
      }
    } else if (message.type === 'result') {
      console.log(`[结果] ${message.subtype}: success`);
    }
  }
}

analyzeProject().catch(console.error);

3.3 调试过程

场景:AI 尝试使用未授权的工具

现象:日志中出现了 AI 调用 Bash 但被拒绝的记录。
原因:你的 prompt 中隐含了需要执行命令的场景(如"运行测试"),
     但 allowedTools 中没有 Bash。
排查:检查 prompt 的内容——是否包含了 required 工具清单之外的操作。
修复(二选一):
  1. 调整 prompt:将需求限制在授权工具能力范围内。
  2. 扩充 allowedTools:将必要工具加入白名单。

四、示例三:钩子系统(Hook)

4.1 原理

钩子(Hook)是 SDK 的安检系统——在 AI 每次调用工具前后拦截请求,允许你做安全检查或日志记录。

两种钩子

钩子类型 触发时机 用途
PreToolUse 工具调用之前 检查参数、决定是否拦截
PostToolUse 工具调用之后 记录日志、验证结果

决策返回值

  • { continue: true }:放行,正常执行。
  • { decision: 'block', reason: '...' }:拦截,AI 会收到被拒绝的通知。

4.2 完整代码

// step3-hooks.ts — 带钩子的 SDK 调用
import { query } from '@tencent-ai/agent-sdk';

async function safeCodeGen() {
  const response = query({
    prompt: `为 src/utils.ts 添加 JSDoc 注释,并为每个函数生成单元测试文件`,
    options: {
      maxTurns: 10,
      cwd: './my-project',
      allowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Bash'],
      model: 'claude-sonnet-4-20250514',
    },
    hooks: {
      // ============================================
      // PreToolUse:工具调用之前的安全检查
      // ============================================
      PreToolUse: [
        {
          // 匹配所有写操作工具
          matcher: /Write|Edit|MultiEdit/,
          handler: async (tool) => {
            const filePath = tool.path || '';

            // 规则 1:禁止修改 .env 和配置文件
            if (filePath.match(/\.env|config\.(json|yaml|yml)/)) {
              return {
                decision: 'block',
                reason: `安全策略:禁止修改配置文件 ${filePath}`,
              };
            }

            // 规则 2:只能写入 src/ 或 __tests__/ 目录
            if (!filePath.startsWith('src/') && !filePath.startsWith('__tests__/')) {
              return {
                decision: 'block',
                reason: `路径策略:只允许写入 src/ 和 __tests__/ 目录,实际路径: ${filePath}`,
              };
            }

            // 规则 3:禁止删除文件(Write 空内容 = 删除)
            if (tool.name === 'Write' && !tool.content) {
              return {
                decision: 'block',
                reason: `安全策略:禁止通过清空内容的方式删除文件 ${filePath}`,
              };
            }

            // 放行
            return { continue: true };
          }
        },
        {
          // 匹配 Bash 执行
          matcher: /Bash/,
          handler: async (tool) => {
            const command = tool.command || '';

            // 危险命令黑名单
            const dangerous = ['rm -rf', 'sudo', 'chmod 777', '> /dev/', ':(){'];
            const matched = dangerous.find((d) => command.includes(d));

            if (matched) {
              return {
                decision: 'block',
                reason: `安全策略:禁止执行危险命令(检测到: ${matched}`,
              };
            }

            return { continue: true };
          }
        }
      ],

      // ============================================
      // PostToolUse:工具调用之后的日志记录
      // ============================================
      PostToolUse: [
        {
          // 匹配所有工具
          matcher: /.*/,
          handler: async (tool, result) => {
            const status = result?.success ? '✅' : '❌';
            const errorMsg = result?.error ? ` (${result.error})` : '';

            console.log(
              `[Hook] ${status} ${tool.name}: ${JSON.stringify(tool.input).substring(0, 80)}${errorMsg}`
            );

            return { continue: true };
          }
        }
      ],
    },
  });

  // 处理响应...
  for await (const message of response) {
    if (message.type === 'system' && message.subtype === 'tool_blocked') {
      console.log(`[拦截] ${message.reason}`);
    }
  }
}

safeCodeGen().catch(console.error);

4.3 预期输出

[Hook] ✅ Read: {"path":"src/utils.ts"}
[Hook] ✅ Glob: {"pattern":"src/**/*.ts"}
[Hook] ✅ Write: {"path":"src/utils.ts","content":"..."}
[拦截] 路径策略:只允许写入 src/ 和 __tests__/ 目录,实际路径: /tmp/temp.js
[Hook] ❌ Write: {"path":"/tmp/temp.js"} (blocked by security policy)
[Hook] ✅ Write: {"path":"__tests__/utils.test.ts","content":"..."}
[Hook] ✅ Bash: {"command":"npx jest __tests__/utils.test.ts"}

4.4 调试过程

场景一:钩子误拦截了正常操作

现象:AI 无法写入 src/components/ 目录下的文件,全部被拦截。
原因:matcher 正则表达式写得太宽或路径检查逻辑有误。
排查:
  1. 检查 PreToolUse handler 中的路径判断逻辑。
  2. 添加临时 console.log 输出实际 path 值,对比 matcher 是否匹配。
修复:
  1. 调整正则或路径前缀判断。
  2. 如果是路径规范化问题(Windows `\` vs Unix `/`),统一规范化路径。

场景二:PostToolUse 中获取不到 result

现象:PostToolUse handler 中 result 为 undefined。
原因:某些系统消息类型不包含 result 字段。
排查:在 handler 开头加类型判断。
修复:
  if (!result) {
    console.log(`[Hook] ${tool.name}: 无结果(系统消息)`);
    return { continue: true };
  }

五、完整实战:档案生成器

5.1 场景

综合运用前三步知识,构建一个自动化简历生成工具。

5.2 完整代码

// profile-builder.ts — 完整实战:自动化简历生成
import { query } from '@tencent-ai/agent-sdk';
import { existsSync, mkdirSync } from 'fs';

async function buildProfile(personName: string) {
  const outputDir = `./output/${personName}`;

  // 确保输出目录存在
  if (!existsSync(outputDir)) {
    mkdirSync(outputDir, { recursive: true });
  }

  console.log(`🔍 正在研究:${personName}\n`);

  const response = query({
    prompt: `请研究 ${personName} 的职业背景,生成专业的中文单页简历。

执行步骤:
1. 使用 WebSearch 搜索 LinkedIn、GitHub、新闻报道中关于 ${personName} 的信息
2. 整理职位经历、技能、教育背景、主要成就
3. 使用 Write 在 ${outputDir}/generate-resume.js 中创建简历生成脚本
4. 使用 Bash 执行 node ${outputDir}/generate-resume.js 输出 resume.md
5. 最终简历以 Markdown 格式保存`,

    options: {
      maxTurns: 15,
      cwd: process.cwd(),
      allowedTools: ['WebSearch', 'Write', 'Bash', 'Read'],
      model: 'claude-sonnet-4-20250514',
    },

    hooks: {
      PreToolUse: [
        {
          // 安全检查:限制写入范围在 output 目录内
          matcher: /Write/,
          handler: async (tool) => {
            const path = tool.path || '';
            if (!path.startsWith('output/')) {
              return {
                decision: 'block',
                reason: `安全策略:只允许写入 output/ 目录,实际路径: ${path}`,
              };
            }
            return { continue: true };
          }
        },
        {
          // 限制 Bash 命令:只允许 node 和 npm 相关命令
          matcher: /Bash/,
          handler: async (tool) => {
            const cmd = tool.command || '';
            if (!cmd.startsWith('node ') && !cmd.startsWith('npm ')) {
              return {
                decision: 'block',
                reason: `安全策略:只允许执行 node/npm 命令,实际命令: ${cmd.substring(0, 50)}`,
              };
            }
            return { continue: true };
          }
        }
      ],

      PostToolUse: [
        {
          matcher: /.*/,
          handler: async (tool, result) => {
            const status = result?.success ? '✅' : '❌';
            console.log(`${status} ${tool.name}: ${JSON.stringify(tool.input).substring(0, 80)}`);
            return { continue: true };
          }
        }
      ],
    },
  });

  // 处理流式响应
  for await (const message of response) {
    if (message.type === 'assistant') {
      for (const block of message.content) {
        if (block.type === 'text') {
          // 只打印摘要,避免刷屏
          const summary = block.text.length > 150
            ? block.text.substring(0, 150) + '...'
            : block.text;
          console.log(`💬 ${summary}`);
        }
      }
    } else if (message.type === 'system' && message.subtype === 'tool_blocked') {
      console.log(`🚫 拦截: ${message.reason}`);
    }
  }

  console.log(`\n📄 简历已生成:${outputDir}/resume.md`);
}

// 从命令行参数获取姓名
const name = process.argv[2] || '张伟';
buildProfile(name).catch((err) => {
  console.error('生成失败:', err.message);
  process.exit(1);
});

运行

tsx profile-builder.ts "张伟"

5.3 预期执行流程

🔍 正在研究:张伟

✅ WebSearch: {"query":"张伟 职业背景 LinkedIn"}
✅ WebSearch: {"query":"张伟 GitHub"}
💬 已找到张伟的相关信息:曾任职于某科技公司...
✅ Write: {"path":"output/张伟/generate-resume.js"}
🚫 拦截: 安全策略:只允许写入 output/ 目录,实际路径: /tmp/temp.js
✅ Bash: {"command":"node output/张伟/generate-resume.js"}
💬 简历 Markdown 已生成...

📄 简历已生成:output/张伟/resume.md

5.4 调试过程

场景一:WebSearch 返回结果不相关

现象:搜索"张伟 职业背景"返回了大量不相关的结果。
原因:"张伟"是常见名,搜索结果被稀释。
排查:
  1. 检查搜索关键词是否足够具体。
  2. 是否有更精确的标识符(如公司名、头衔)可以限定范围?
修复:在 prompt 中加入更具体的限定条件,如:
  "搜索时加上限定词:技术总监、前阿里巴巴" 等。

场景二:生成的简历内容质量问题

现象:AI 编造了不存在的经历。
原因:AI 搜索不到足够信息时,倾向于"填补空白"(幻觉)。
排查:逐条检查简历中的经历是否能在 WebSearch 结果中找到来源。
修复:
  1. 在 prompt 中要求 AI 标注信息来源(每条经历附上来源 URL)。
  2. 设置更严格的 Acceptance 规则。

六、Session API:保持上下文的连续对话

6.1 原理

query() 每次调用是独立的——不保留上下文。Session API 则维持一个长期对话,AI 记住之前的讨论内容,适合多轮迭代的场景。

6.2 示例代码

// session-example.ts
import { createSession } from '@tencent-ai/agent-sdk';

async function iterativeRefactor() {
  // 创建会话
  const session = await createSession({
    options: {
      cwd: './my-project',
      model: 'claude-sonnet-4-20250514',
      maxTurns: 8,
    }
  });

  // 第一轮:分析架构
  console.log('轮次 1:分析项目架构');
  for await (const msg of await session.send('分析当前项目的目录结构和模块划分')) {
    // 处理消息...
  }

  // 第二轮:AI 记得上一轮的分析结果
  console.log('\n轮次 2:基于刚才的分析,给出重构建议');
  for await (const msg of await session.send('基于刚才的分析,给出重构建议')) {
    // AI 会引用第一轮中了解到的模块名称和结构
  }

  // 第三轮:细化某个具体建议
  console.log('\n轮次 3:针对模块拆分的建议,展开具体方案');
  for await (const msg of await session.send('针对模块拆分的建议,展开具体的实施步骤')) {
    // AI 记得前两轮的上下文
  }

  // 关闭会话
  await session.close();
}

iterativeRefactor().catch(console.error);

6.3 调试过程

场景:Session 对话过长导致质量下降

现象:Session 到第 8 轮以后,AI 的回复质量明显下降。
原因:上下文窗口积压了过多历史信息,关键信息被稀释。
排查:观察对话轮次,超过 10 轮考虑做上下文裁剪。
修复:
  1. 在第 5-6 轮时,让 AI 输出一个"上下文摘要",后续轮次基于摘要继续。
  2. 或者关闭当前 Session,创建新 Session,用摘要作为初始 prompt。

七、小结

SDK 本质:在代码里调用 AI,而不是在终端对话
四步递进:
  1. query() 最简调用      → 理解消息流
  2. allowedTools 工具控制  → 限制 AI 操作范围
  3. PreToolUse/PostToolUse → 安检 + 日志
  4. 完整应用:档案生成器   → 综合实战

query():单次独立任务,无上下文记忆
Session:多轮连续对话,AI 记住历史
Hook:安全检查 + 日志审计,必不可少
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐