如何手写一个简单的OpenClaw
文章摘要 本文介绍了如何用500行代码实现一个简化版OpenClaw AI Agent框架,帮助开发者深入理解AI Agent的核心原理。作者通过三层架构设计(Agent Core、Memory和LLM Client),聚焦实现智能体循环、工具调用等核心功能,同时做了适当简化。文章详细展示了类型定义、LLM客户端实现等关键代码片段,旨在让读者从"理解原理"进阶到"动手
如何手写一个简单的 OpenClaw:从零实现 AI Agent 框架
通过 500 行代码实现一个简化版 OpenClaw,彻底理解 AI Agent 的核心原理
开篇
最近 OpenClaw 火得一塌糊涂,24 万 Star 超越 React 成为 GitHub 历史第一。作为一个 10 年前端老兵,我花了几天时间研究它的架构,写了一篇《OpenClaw 底层原理深度解析》。
但看懂架构是一回事,能不能自己写出来是另一回事。
于是我决定挑战自己:用 500 行代码手写一个简化版 OpenClaw。
这个过程让我对 AI Agent 的理解从"知道怎么回事"变成了"知道怎么做"。这篇文章就是我的实现过程分享。
这篇文章能帮你:
- 从零实现一个可运行的 AI Agent 框架
- 理解 Agent Loop、Tool Calling、Memory 的核心原理
- 掌握流式响应、错误处理等实战技巧
- 获得一份可以直接跑起来的代码
前置要求:
- 熟悉 Node.js 和 TypeScript
- 了解 AI API 的基本调用方式
- 最好先看过我之前的《OpenClaw 底层原理深度解析》
设计目标:做减法
OpenClaw 功能很多,我们不可能全部实现。核心目标是抓住本质:
| OpenClaw 完整功能 | 我们的简化版 |
|---|---|
| 支持 10+ 聊天平台 | 只支持命令行 |
| 复杂的 Memory 系统 | 简单的内存数组 |
| 插件式 Skills | 内置几个工具 |
| WebSocket 服务器 | 直接函数调用 |
| 沙箱安全执行 | 基础的命令过滤 |
| 多轮迭代控制 | 简单的最大轮数 |
但核心的 Agent Loop(智能体循环)我们会完整实现,这是 AI Agent 的灵魂。
架构设计:三层结构
┌─────────────────────────────────────────────────┐
│ MiniClaw │
│ (我们的简化版 OpenClaw) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Layer 1: Agent Core │
│ - Agent Loop (思考-执行循环) │
│ - Tool Executor (工具执行器) │
│ - Message Builder (消息构建器) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Layer 2: Memory │
│ - Conversation History (对话历史) │
│ - Tool Results (工具结果) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Layer 3: LLM Client │
│ - API 调用封装 │
│ - 流式响应处理 │
│ - 错误重试 │
└─────────────────────────────────────────────────┘
第一步:定义核心类型
先定义好 TypeScript 类型,让代码更清晰:
// types.ts
// 消息类型
interface Message {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string;
tool_call_id?: string; // 工具调用的 ID
tool_calls?: ToolCall[]; // 助手请求的工具调用
}
// 工具调用
interface ToolCall {
id: string;
type: 'function';
function: {
name: string;
arguments: string; // JSON 字符串
};
}
// 工具定义
interface Tool {
name: string;
description: string;
parameters: {
type: 'object';
properties: Record<string, {
type: string;
description: string;
}>;
required: string[];
};
execute: (args: Record<string, any>) => Promise<string>;
}
// Agent 配置
interface AgentConfig {
model: string;
apiKey: string;
apiEndpoint?: string;
systemPrompt: string;
tools: Tool[];
maxIterations: number; // 最大循环次数,防止无限循环
}
第二步:实现 LLM 客户端
LLM 客户端负责调用 AI API,支持流式响应:
// llm-client.ts
import { Message, ToolCall } from './types';
interface LLMResponse {
content: string;
toolCalls: ToolCall[];
finishReason: 'stop' | 'tool_calls' | 'length';
}
class LLMClient {
private apiKey: string;
private apiEndpoint: string;
private model: string;
constructor(config: {
apiKey: string;
apiEndpoint?: string;
model: string;
}) {
this.apiKey = config.apiKey;
this.apiEndpoint = config.apiEndpoint || 'https://api.anthropic.com/v1/messages';
this.model = config.model;
}
async chat(
messages: Message[],
tools: any[],
onChunk?: (chunk: string) => void
): Promise<LLMResponse> {
// 转换消息格式(适配 Claude API)
const formattedMessages = this.formatMessages(messages);
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: this.model,
max_tokens: 4096,
messages: formattedMessages,
tools: this.formatTools(tools),
stream: true // 启用流式响应
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`API 调用失败: ${error}`);
}
// 处理流式响应
return this.handleStream(response, onChunk);
}
private formatMessages(messages: Message[]): any[] {
// 将我们的消息格式转换为 Claude API 格式
const result: any[] = [];
for (const msg of messages) {
if (msg.role === 'system') {
// Claude 的 system 放在请求根级别,这里跳过
continue;
}
if (msg.role === 'tool') {
// 工具结果
result.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: msg.content
}]
});
} else if (msg.tool_calls && msg.tool_calls.length > 0) {
// 助手的工具调用
result.push({
role: 'assistant',
content: msg.tool_calls.map(tc => ({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: JSON.parse(tc.function.arguments)
}))
});
} else {
result.push({
role: msg.role,
content: msg.content
});
}
}
return result;
}
private formatTools(tools: any[]): any[] {
// 转换为 Claude 的工具格式
return tools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.parameters
}));
}
private async handleStream(
response: Response,
onChunk?: (chunk: string) => void
): Promise<LLMResponse> {
const reader = response.body?.getReader();
if (!reader) throw new Error('无法读取响应流');
const decoder = new TextDecoder();
let content = '';
let toolCalls: ToolCall[] = [];
let finishReason: 'stop' | 'tool_calls' | 'length' = 'stop';
let currentToolUse: any = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6); // 去掉 'data: ' 前缀
if (data === '[DONE]') continue;
try {
const event = JSON.parse(data);
if (event.type === 'content_block_start') {
if (event.content_block.type === 'tool_use') {
currentToolUse = {
id: event.content_block.id,
name: event.content_block.name,
arguments: ''
};
}
} else if (event.type === 'content_block_delta') {
if (event.delta.type === 'text_delta') {
content += event.delta.text;
onChunk?.(event.delta.text);
} else if (event.delta.type === 'input_json_delta') {
if (currentToolUse) {
currentToolUse.arguments += event.delta.partial_json;
}
}
} else if (event.type === 'content_block_stop') {
if (currentToolUse) {
toolCalls.push({
id: currentToolUse.id,
type: 'function',
function: {
name: currentToolUse.name,
arguments: currentToolUse.arguments
}
});
currentToolUse = null;
}
} else if (event.type === 'message_delta') {
if (event.delta.stop_reason === 'tool_use') {
finishReason = 'tool_calls';
}
}
} catch (e) {
// 忽略解析错误,继续处理
}
}
}
return { content, toolCalls, finishReason };
}
}
export { LLMClient, LLMResponse };
这段代码的关键点:
- 流式响应处理:使用
ReadableStream逐块读取,实现打字机效果 - 消息格式转换:将我们的标准格式转换为 Claude API 格式
- 工具调用解析:从流中正确提取工具调用信息
第三步:实现内置工具
我们实现几个基础但实用的工具:
// tools.ts
import { Tool } from './types';
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as path from 'path';
const execAsync = promisify(exec);
// 安全命令白名单
const SAFE_COMMANDS = ['ls', 'cat', 'head', 'tail', 'grep', 'wc', 'date', 'pwd', 'echo'];
// 危险模式黑名单
const DANGEROUS_PATTERNS = [
/rm\s+-rf/,
/>\s*\//,
/;\s*rm/,
/\|\s*sh/,
/\$\(/,
/`/
];
function isSafeCommand(cmd: string): boolean {
// 检查危险模式
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(cmd)) {
return false;
}
}
// 检查命令是否在白名单
const baseCommand = cmd.trim().split(/\s+/)[0];
return SAFE_COMMANDS.includes(baseCommand);
}
// 工具1: 执行 Shell 命令
const shellTool: Tool = {
name: 'execute_shell',
description: '执行 shell 命令。只能执行安全的只读命令,如 ls、cat、grep 等。',
parameters: {
type: 'object',
properties: {
command: {
type: 'string',
description: '要执行的 shell 命令'
}
},
required: ['command']
},
async execute(args) {
const { command } = args;
if (!isSafeCommand(command)) {
return `错误:该命令不在安全白名单中。只允许执行:${SAFE_COMMANDS.join(', ')}`;
}
try {
const { stdout, stderr } = await execAsync(command, {
timeout: 30000, // 30 秒超时
maxBuffer: 1024 * 1024 // 1MB 缓冲区
});
return stdout || stderr || '命令执行成功(无输出)';
} catch (error: any) {
return `命令执行失败:${error.message}`;
}
}
};
// 工具2: 读取文件
const readFileTool: Tool = {
name: 'read_file',
description: '读取指定文件的内容',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: '文件路径'
},
maxLines: {
type: 'number',
description: '最多读取的行数,默认 100'
}
},
required: ['path']
},
async execute(args) {
const { path: filePath, maxLines = 100 } = args;
try {
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n');
if (lines.length > maxLines) {
return lines.slice(0, maxLines).join('\n') +
`\n\n... 文件共 ${lines.length} 行,只显示前 ${maxLines} 行`;
}
return content;
} catch (error: any) {
return `读取文件失败:${error.message}`;
}
}
};
// 工具3: 写入文件
const writeFileTool: Tool = {
name: 'write_file',
description: '将内容写入指定文件',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: '文件路径'
},
content: {
type: 'string',
description: '要写入的内容'
}
},
required: ['path', 'content']
},
async execute(args) {
const { path: filePath, content } = args;
// 安全检查:不允许写入系统目录
const absolutePath = path.resolve(filePath);
if (absolutePath.startsWith('/etc') ||
absolutePath.startsWith('/usr') ||
absolutePath.startsWith('/bin')) {
return '错误:不允许写入系统目录';
}
try {
// 确保目录存在
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, content, 'utf-8');
return `文件已写入:${absolutePath}`;
} catch (error: any) {
return `写入文件失败:${error.message}`;
}
}
};
// 工具4: 网络请求
const fetchTool: Tool = {
name: 'fetch_url',
description: '获取指定 URL 的内容',
parameters: {
type: 'object',
properties: {
url: {
type: 'string',
description: '要请求的 URL'
},
method: {
type: 'string',
description: 'HTTP 方法,默认 GET'
}
},
required: ['url']
},
async execute(args) {
const { url, method = 'GET' } = args;
try {
const response = await fetch(url, { method });
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const json = await response.json();
return JSON.stringify(json, null, 2);
}
const text = await response.text();
// 限制返回长度
if (text.length > 5000) {
return text.substring(0, 5000) + '\n\n... 内容过长,已截断';
}
return text;
} catch (error: any) {
return `请求失败:${error.message}`;
}
}
};
// 工具5: 计算器
const calculatorTool: Tool = {
name: 'calculator',
description: '执行数学计算',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: '数学表达式,如 "2 + 2 * 3"'
}
},
required: ['expression']
},
async execute(args) {
const { expression } = args;
// 安全检查:只允许数字和运算符
if (!/^[\d\s+\-*/().]+$/.test(expression)) {
return '错误:表达式包含不允许的字符';
}
try {
// 使用 Function 构造器计算(比 eval 稍安全)
const result = new Function(`return ${expression}`)();
return `计算结果:${expression} = ${result}`;
} catch (error: any) {
return `计算失败:${error.message}`;
}
}
};
// 导出所有工具
export const builtinTools: Tool[] = [
shellTool,
readFileTool,
writeFileTool,
fetchTool,
calculatorTool
];
安全设计的几个要点:
- 命令白名单:只允许执行预定义的安全命令
- 危险模式检测:阻止
rm -rf、命令注入等危险操作 - 路径限制:禁止写入系统目录
- 超时控制:防止命令执行过长
- 输出限制:防止返回过多内容消耗 token
第四步:实现 Agent Loop(核心!)
这是整个框架的灵魂——Agent 循环:
// agent.ts
import { LLMClient } from './llm-client';
import { Tool, Message, AgentConfig, ToolCall } from './types';
import { builtinTools } from './tools';
class MiniClawAgent {
private llm: LLMClient;
private tools: Map<string, Tool>;
private config: AgentConfig;
private memory: Message[];
constructor(config: AgentConfig) {
this.config = config;
this.llm = new LLMClient({
apiKey: config.apiKey,
apiEndpoint: config.apiEndpoint,
model: config.model
});
// 注册工具
this.tools = new Map();
for (const tool of [...builtinTools, ...config.tools]) {
this.tools.set(tool.name, tool);
}
// 初始化记忆(对话历史)
this.memory = [
{ role: 'system', content: this.buildSystemPrompt() }
];
}
// 构建系统提示词
private buildSystemPrompt(): string {
const toolDescriptions = Array.from(this.tools.values())
.map(t => `- ${t.name}: ${t.description}`)
.join('\n');
return `${this.config.systemPrompt}
你可以使用以下工具来完成任务:
${toolDescriptions}
重要规则:
1. 当需要执行操作时,使用工具而不是假装执行
2. 每次只调用必要的工具,避免过度调用
3. 在回复用户前,确保已完成所有必要的操作
4. 如果工具执行失败,尝试其他方法或告知用户
当前时间:${new Date().toISOString()}
`;
}
// 核心:Agent 循环
async run(
userMessage: string,
onChunk?: (chunk: string) => void
): Promise<string> {
// 1. 添加用户消息到记忆
this.memory.push({ role: 'user', content: userMessage });
let iteration = 0;
let finalResponse = '';
// Agent 循环
while (iteration < this.config.maxIterations) {
iteration++;
console.log(`\n--- 第 ${iteration} 轮迭代 ---`);
// 2. 调用 LLM
const response = await this.llm.chat(
this.memory,
this.getToolDefinitions(),
onChunk
);
// 3. 根据响应类型决定下一步
if (response.finishReason === 'stop') {
// 没有工具调用,任务完成
finalResponse = response.content;
this.memory.push({ role: 'assistant', content: response.content });
break;
}
if (response.finishReason === 'tool_calls' && response.toolCalls.length > 0) {
// 有工具调用,执行工具
console.log(`发现 ${response.toolCalls.length} 个工具调用`);
// 记录助手的工具调用请求
this.memory.push({
role: 'assistant',
content: response.content,
tool_calls: response.toolCalls
});
// 4. 执行所有工具调用
for (const toolCall of response.toolCalls) {
console.log(`执行工具: ${toolCall.function.name}`);
const result = await this.executeTool(toolCall);
// 将工具结果添加到记忆
this.memory.push({
role: 'tool',
content: result,
tool_call_id: toolCall.id
});
}
// 继续循环,让 LLM 处理工具结果
continue;
}
// 其他情况(如达到 token 限制),退出循环
finalResponse = response.content || '任务处理异常';
break;
}
if (iteration >= this.config.maxIterations) {
finalResponse += '\n\n(已达到最大迭代次数,任务可能未完成)';
}
return finalResponse;
}
// 执行单个工具
private async executeTool(toolCall: ToolCall): Promise<string> {
const tool = this.tools.get(toolCall.function.name);
if (!tool) {
return `错误:未找到工具 "${toolCall.function.name}"`;
}
try {
const args = JSON.parse(toolCall.function.arguments);
console.log(`工具参数:`, args);
const result = await tool.execute(args);
console.log(`工具结果:`, result.substring(0, 200) + (result.length > 200 ? '...' : ''));
return result;
} catch (error: any) {
console.error(`工具执行错误:`, error);
return `工具执行失败:${error.message}`;
}
}
// 获取工具定义(用于 LLM 调用)
private getToolDefinitions(): any[] {
return Array.from(this.tools.values()).map(tool => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters
}));
}
// 获取对话历史
getHistory(): Message[] {
return [...this.memory];
}
// 清空对话历史
clearHistory(): void {
this.memory = [
{ role: 'system', content: this.buildSystemPrompt() }
];
}
}
export { MiniClawAgent };
Agent Loop 的核心逻辑图解:
用户输入
↓
┌──────────────────────────────────────┐
│ Agent Loop │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 1. 添加用户消息到 Memory │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ 2. 调用 LLM(带 Memory + Tools)│ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ 3. 检查响应类型 │ │
│ │ ├─ stop → 返回最终回复 │ │
│ │ └─ tool_calls → 执行工具 │ │
│ └─────────────────────────────────┘ │
│ ↓ (如果是 tool_calls) │
│ ┌─────────────────────────────────┐ │
│ │ 4. 执行工具,结果加入 Memory │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ 回到步骤 2 │
└──────────────────────────────────────┘
↓
最终回复
这就是 AI Agent 的本质:不断循环 “思考 → 行动 → 观察” 直到完成任务。
第五步:实现命令行界面
最后,我们需要一个交互界面:
// cli.ts
import * as readline from 'readline';
import { MiniClawAgent } from './agent';
async function main() {
// 从环境变量获取 API Key
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error('请设置 ANTHROPIC_API_KEY 环境变量');
process.exit(1);
}
// 创建 Agent 实例
const agent = new MiniClawAgent({
model: 'claude-sonnet-4-20250514',
apiKey,
systemPrompt: `你是 MiniClaw,一个简化版的 AI Agent。
你可以帮助用户完成各种任务,包括:
- 执行 shell 命令查看系统信息
- 读取和写入文件
- 进行网络请求
- 数学计算
请简洁、准确地回答问题,必要时使用工具完成任务。`,
tools: [], // 使用内置工具
maxIterations: 10
});
// 创建命令行界面
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('========================================');
console.log('🤖 MiniClaw - 简化版 OpenClaw');
console.log('========================================');
console.log('输入消息与 Agent 对话,输入 "exit" 退出');
console.log('输入 "clear" 清空对话历史');
console.log('----------------------------------------\n');
const prompt = () => {
rl.question('你: ', async (input) => {
const trimmed = input.trim();
if (trimmed === 'exit') {
console.log('再见!');
rl.close();
return;
}
if (trimmed === 'clear') {
agent.clearHistory();
console.log('对话历史已清空\n');
prompt();
return;
}
if (!trimmed) {
prompt();
return;
}
try {
process.stdout.write('MiniClaw: ');
const response = await agent.run(trimmed, (chunk) => {
// 流式输出
process.stdout.write(chunk);
});
// 如果没有流式输出,直接打印完整响应
if (!response.startsWith('MiniClaw: ')) {
console.log(); // 换行
}
console.log('\n');
} catch (error: any) {
console.error(`\n错误: ${error.message}\n`);
}
prompt();
});
};
prompt();
}
main();
运行效果演示
编译并运行:
# 安装依赖
npm init -y
npm install typescript @types/node
npx tsc --init
# 编译
npx tsc
# 运行(需要设置 API Key)
export ANTHROPIC_API_KEY="your-api-key"
node dist/cli.js
实际对话演示:
========================================
🤖 MiniClaw - 简化版 OpenClaw
========================================
输入消息与 Agent 对话,输入 "exit" 退出
输入 "clear" 清空对话历史
----------------------------------------
你: 帮我看看当前目录有什么文件
--- 第 1 轮迭代 ---
发现 1 个工具调用
执行工具: execute_shell
工具参数: { command: 'ls -la' }
工具结果: total 32
drwxr-xr-x 8 user staff 256 Mar 13 10:00 .
drwxr-xr-x 5 user staff 160 Mar 13 09:00 ..
-rw-r--r-- 1 user staff 1234 Mar 13 10:00 agent.ts
-rw-r--r-- 1 user staff 567 Mar 13 10:00 cli.ts
...
--- 第 2 轮迭代 ---
MiniClaw: 当前目录下有以下文件:
1. `agent.ts` - Agent 核心代码
2. `cli.ts` - 命令行界面
3. `llm-client.ts` - LLM 客户端
4. `tools.ts` - 工具定义
5. `types.ts` - 类型定义
6. `package.json` - 项目配置
你: 123 * 456 + 789 等于多少?
--- 第 1 轮迭代 ---
发现 1 个工具调用
执行工具: calculator
工具参数: { expression: '123 * 456 + 789' }
工具结果: 计算结果:123 * 456 + 789 = 56877
--- 第 2 轮迭代 ---
MiniClaw: 123 * 456 + 789 = 56877
你: 写一个 hello.txt 文件,内容是 "Hello, MiniClaw!"
--- 第 1 轮迭代 ---
发现 1 个工具调用
执行工具: write_file
工具参数: { path: 'hello.txt', content: 'Hello, MiniClaw!' }
工具结果: 文件已写入:/Users/xxx/projects/miniclaw/hello.txt
--- 第 2 轮迭代 ---
MiniClaw: 已创建 hello.txt 文件,内容为 "Hello, MiniClaw!"
在英博云平台部署
如果你想把这个 MiniClaw 部署到服务器,在英博云平台可以这样做:
Docker 部署配置
# Dockerfile
FROM node:20-alpine
WORKDIR /app
# 复制项目文件
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 运行
CMD ["node", "dist/cli.js"]
部署步骤
# 1. 构建镜像
docker build -t miniclaw .
# 2. 在英博云平台创建容器实例
# 配置环境变量 ANTHROPIC_API_KEY
# 3. 运行
docker run -it \
-e ANTHROPIC_API_KEY="your-key" \
miniclaw
Web API 版本
如果想通过 HTTP 调用,可以加一个简单的 Express 服务:
// server.ts
import express from 'express';
import { MiniClawAgent } from './agent';
const app = express();
app.use(express.json());
// 存储会话
const sessions = new Map<string, MiniClawAgent>();
function getOrCreateAgent(sessionId: string): MiniClawAgent {
if (!sessions.has(sessionId)) {
sessions.set(sessionId, new MiniClawAgent({
model: 'claude-sonnet-4-20250514',
apiKey: process.env.ANTHROPIC_API_KEY!,
systemPrompt: '你是 MiniClaw,一个有用的 AI 助手。',
tools: [],
maxIterations: 10
}));
}
return sessions.get(sessionId)!;
}
app.post('/chat', async (req, res) => {
const { sessionId, message } = req.body;
if (!sessionId || !message) {
return res.status(400).json({ error: '缺少 sessionId 或 message' });
}
const agent = getOrCreateAgent(sessionId);
try {
const response = await agent.run(message);
res.json({ response });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`MiniClaw 服务运行在 http://localhost:${PORT}`);
});
踩坑分享
问题 1:流式响应解析错乱
现象: 工具调用的 JSON 参数解析失败
原因: Claude 的流式响应中,工具参数是逐字符发送的,需要累积拼接
解决:
// 错误做法
if (event.delta.type === 'input_json_delta') {
const args = JSON.parse(event.delta.partial_json); // 这会失败!
}
// 正确做法
let argumentsBuffer = '';
if (event.delta.type === 'input_json_delta') {
argumentsBuffer += event.delta.partial_json; // 累积
}
// 在 content_block_stop 时才解析
if (event.type === 'content_block_stop') {
const args = JSON.parse(argumentsBuffer); // 完整了再解析
}
问题 2:工具调用后 LLM 不响应
现象: 执行工具后,LLM 没有生成回复
原因: 消息格式不对,Claude 需要特定的 tool_result 格式
解决:
// 错误做法
messages.push({
role: 'assistant',
content: `工具结果:${result}`
});
// 正确做法
messages.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: toolCallId,
content: result
}]
});
问题 3:无限循环
现象: Agent 一直调用工具,停不下来
原因:
- 系统提示词没有明确告诉 Agent 何时停止
- 没有设置最大迭代次数
解决:
// 1. 在系统提示词中明确
const systemPrompt = `
...
当任务完成时,直接回复用户,不要继续调用工具。
如果无法完成任务,说明原因并停止。
`;
// 2. 设置最大迭代次数
const MAX_ITERATIONS = 10;
if (iteration >= MAX_ITERATIONS) {
return '已达到最大迭代次数,任务可能未完成';
}
问题 4:Token 消耗过快
现象: 几轮对话就消耗大量 token
原因: 每次都发送完整历史,包括冗长的工具结果
解决:
// 压缩工具结果
function compressToolResult(result: string, maxLength = 500): string {
if (result.length <= maxLength) return result;
return result.substring(0, maxLength) + '\n... (结果已截断)';
}
// 定期压缩历史
function compressHistory(messages: Message[]): Message[] {
if (messages.length < 20) return messages;
// 保留 system 和最近 10 条
const system = messages[0];
const recent = messages.slice(-10);
return [system, { role: 'system', content: '(之前的对话已省略)' }, ...recent];
}
进阶:添加更多能力
基础框架搭好后,可以继续扩展:
1. 添加向量记忆
// 使用 SQLite + 向量扩展
import Database from 'better-sqlite3';
class VectorMemory {
private db: Database.Database;
async store(text: string, embedding: number[]) {
// 存储文本和向量
}
async search(query: string, limit = 5): Promise<string[]> {
// 向量相似度搜索
}
}
2. 添加 Skill 系统
// 动态加载 Markdown 定义的 Skill
async function loadSkills(dir: string): Promise<Tool[]> {
const files = await fs.readdir(dir);
const skills: Tool[] = [];
for (const file of files) {
if (file.endsWith('.md')) {
const content = await fs.readFile(path.join(dir, file), 'utf-8');
const skill = parseSkillMarkdown(content);
skills.push(skill);
}
}
return skills;
}
3. 添加 WebSocket 服务
// 支持外部客户端连接
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 18789 });
wss.on('connection', (ws) => {
const agent = new MiniClawAgent(config);
ws.on('message', async (data) => {
const { type, content } = JSON.parse(data.toString());
if (type === 'message') {
const response = await agent.run(content, (chunk) => {
ws.send(JSON.stringify({ type: 'chunk', content: chunk }));
});
ws.send(JSON.stringify({ type: 'done', content: response }));
}
});
});
总结
通过这 500 行代码,我们实现了一个简化但完整的 AI Agent 框架。核心要点:
技术收获
- Agent Loop 是灵魂:思考 → 行动 → 观察的循环
- 工具是手脚:让 AI 能"做事"而不只是"说话"
- Memory 是记忆:保持对话上下文和状态
- 流式响应是体验:让用户感知到 AI 在"思考"
设计启示
- 做减法:复杂系统的核心往往很简单
- 分层清晰:LLM Client、Agent Core、Tools 各司其职
- 安全第一:工具执行必须有防护
- 渐进增强:先跑起来,再慢慢完善
后续计划
- 添加向量记忆,实现长期记忆
- 实现多 Agent 协作
- 在英博云平台部署完整服务
- 参考 OpenClaw 的 Skill 系统,实现插件化
参考资源
手写一遍比看十遍文档管用。如果你也想深入理解 AI Agent,建议跟着这篇文章自己实现一遍。代码不复杂,但每一行都是理解的过程。
有问题欢迎交流!
更多推荐



所有评论(0)