CodeBuddy 学习(8):Agent SDK
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。
工作原理:
- SDK 在 Node.js 进程中通过
@tencent-ai/agent-sdk包导入。 - 调用
query()时,SDK 启动一个 CodeBuddy CLI 子进程。 - 通过 stdin/stdout 与子进程通信——发送指令、接收结果。
- 结果以流式(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 版本不兼容。
排查:
- 检查 tsconfig.json 中的 target 和 module 设置。
- 检查 @tencent-ai/agent-sdk 的版本。
修复: - 在 tsconfig.json 中设置 “skipLibCheck”: true。
- 或使用 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:安全检查 + 日志审计,必不可少
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)