Appearance
开发 CLI Agent
本文档将指导你如何开发一个 CLI Agent,并通过 Docker 容器的方式接入 Mythra Agent Studio。
概述
CLI Agent 是一种以命令行工具形式发布的 AI Agent 程序。它具有以下特点:
- 封装在 Docker image 中: CLI Agent 打包成 Docker 镜像,便于分发和部署
- 通过 stdio 通信: 使用标准输入(stdin)接收消息,标准输出(stdout)发送响应
- 遵循消息协议: 必须遵循 Agent 消息协议 与前端进行通信
- 运行在沙箱环境: CLI Agent 在沙箱(sandbox)环境中运行,可以访问沙箱文件系统
架构说明
整体架构
通信流程
- 发现 Agent: 前端通过
/agent/:project_id/list接口获取可用的 Agent 列表,其中包含 CLI Agent - 建立连接: 用户选择 CLI Agent 后,前端与
agent-server建立 WebSocket 连接 - 启动容器:
agent-server使用docker run --rm -i命令启动 Docker 容器 - 消息传递:
- WebSocket 上行消息 → CLI Agent 的 stdin
- CLI Agent 的 stdout → WebSocket 下行消息
- CLI Agent 的 stderr → 日志文件
- 结束会话: Agent 处理完成后退出,容器自动删除
开发步骤
1. 选择技术栈
你可以使用任何支持标准输入输出的编程语言开发 CLI Agent。推荐选择:
| 语言 | 推荐框架/库 | 特点 |
|---|---|---|
| Node.js | readline、event-stream | 生态丰富、开发效率高 |
| Python | sys.stdin/stdout、asyncio | AI 生态完善、适合机器学习场景 |
| Go | bufio.Scanner、os.Stdin | 高性能、部署简单 |
| Rust | std::io::Stdin | 高性能、内存安全 |
本文以 Node.js 为例进行说明。
2. 创建项目
创建一个新的 Node.js 项目:
bash
mkdir my-cli-agent
cd my-cli-agent
npm init -y
npm install openai3. 实现消息处理
创建 index.js 文件,实现基本的 stdin/stdout 通信:
javascript
const readline = require('readline');
const OpenAI = require('openai');
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
function generateId() {
return Math.random().toString(36).substring(2, 15);
}
function sendMessage(type, event, content = '', extra = {}) {
const message = {
id: generateId(),
type,
event,
...extra,
};
if (content) {
message.content = content;
}
console.log(JSON.stringify(message));
}
async function processUserInput(userInput) {
sendMessage('response-state', 'start');
sendMessage('iteration-state', 'start', 'iteration 0');
sendMessage('model-invocation-state', 'start');
const chatId = generateId();
sendMessage('chat-output', 'start');
try {
const stream = await openai.chat.completions.create({
model: process.env.OPENAI_MODEL || 'gpt-4',
messages: [{ role: 'user', content: userInput }],
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
sendMessage('chat-output', 'delta', content);
}
}
sendMessage('chat-output', 'end');
} catch (error) {
sendMessage('error', 'error', error.message);
}
sendMessage('model-invocation-state', 'end');
sendMessage('iteration-state', 'end', 'iteration 0');
sendMessage('response-state', 'end');
}
rl.on('line', async (line) => {
try {
const message = JSON.parse(line);
if (message.type === 'user-input') {
await processUserInput(message.userInput);
}
} catch (error) {
sendMessage('error', 'error', `Failed to process message: ${error.message}`);
}
});4. 创建 Dockerfile
创建 Dockerfile 文件:
dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
CMD ["node", "index.js"]5. 构建和测试
构建 Docker 镜像:
bash
docker build -t my-cli-agent:latest .本地测试:
bash
echo '{"type":"user-input","userInput":"你好","sessionId":"test-session"}' | \
docker run --rm -i \
-e OPENAI_API_KEY=your-api-key \
-e OPENAI_BASE_URL=https://api.openai.com/v1 \
-e OPENAI_MODEL=gpt-4 \
my-cli-agent:latest配置说明
docker.yaml 配置格式
CLI Agent 需要在 agent-server 的 etc/docker.yaml 配置文件中注册。配置格式如下:
yaml
builtin_agents:
- id: my-cli-agent
name: "我的CLI Agent"
proto: MAP-V1
image: my-cli-agent:latest
command: node
args:
- index.js配置字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Agent 的唯一标识符,用于 API 调用 |
name | string | 是 | Agent 的显示名称,显示在前端 UI 中 |
proto | string | 是 | 协议版本,目前固定为 MAP-V1 |
image | string | 是 | Docker 镜像名称 |
command | string | 是 | 容器启动命令 |
args | []string | 否 | 命令参数列表 |
环境变量
agent-server 会自动注入以下环境变量到 Docker 容器中:
| 环境变量 | 说明 |
|---|---|
OPENAI_BASE_URL | OpenAI API 的 Base URL |
OPENAI_API_KEY | OpenAI API Key |
OPENAI_MODEL | 用户选择的模型名称 |
SESSION_ID | 会话 ID |
RUN_ID | 本次运行的唯一 ID |
TZ | 时区设置,默认为 Asia/Shanghai |
挂载目录
CLI Agent 运行时,沙箱目录会被挂载到容器的 /data 目录:
bash
-v /path/to/sandbox:/dataAgent 可以在 /data 目录中读写文件,这些文件会同步到用户的沙箱环境中。
消息协议
CLI Agent 必须遵循 Agent 消息协议 进行通信。以下是关键要点:
输入消息格式
从 stdin 接收的消息格式:
json
{
"type": "user-input",
"apiKey": "your-wds-api-key",
"projectId": "2231",
"workDir": "/data",
"sessionId": "d271487972e2221eb3aa98bc1a7ae6fc",
"userInput": "你好",
"preferredModel": "火山引擎:doubao-seed-1-6-lite-251015"
}输出消息格式
向 stdout 输出的消息格式:
响应开始:
json
{"id":"abc123","type":"response-state","event":"start"}聊天内容:
json
{"id":"def456","type":"chat-output","event":"start"}
{"id":"def456","type":"chat-output","event":"delta","content":"你好"}
{"id":"def456","type":"chat-output","event":"delta","content":"!"}
{"id":"def456","type":"chat-output","event":"end"}响应结束:
json
{"id":"abc123","type":"response-state","event":"end"}消息流示例
一个完整的消息流示例:
stdin: {"type":"user-input","userInput":"你好",...}
stdout: {"id":"a1","type":"response-state","event":"start"}
stdout: {"id":"b1","type":"iteration-state","event":"start","content":"iteration 0"}
stdout: {"id":"c1","type":"model-invocation-state","event":"start"}
stdout: {"id":"d1","type":"chat-output","event":"start"}
stdout: {"id":"d1","type":"chat-output","event":"delta","content":"你好"}
stdout: {"id":"d1","type":"chat-output","event":"delta","content":"!"}
stdout: {"id":"d1","type":"chat-output","event":"end"}
stdout: {"id":"c1","type":"model-invocation-state","event":"end"}
stdout: {"id":"b1","type":"iteration-state","event":"end","content":"iteration 0"}
stdout: {"id":"a1","type":"response-state","event":"end"}进阶功能
访问沙箱文件
CLI Agent 可以直接访问挂载在 /data 目录下的沙箱文件:
javascript
const fs = require('fs');
const path = require('path');
const workDir = '/data';
function readFile(filename) {
const filePath = path.join(workDir, filename);
return fs.readFileSync(filePath, 'utf-8');
}
function writeFile(filename, content) {
const filePath = path.join(workDir, filename);
fs.writeFileSync(filePath, content, 'utf-8');
}调用工具
当 Agent 执行工具调用(如文件操作、命令执行等)时,可以发送 tool-call 消息来通知用户。这是一种通知机制,让用户了解 Agent 的工作过程,增强透明度和用户体验。
tool-call 消息会在前端 UI 中显示,用户可以看到 Agent 正在调用什么工具以及调用的参数。
javascript
function callTool(func, args) {
const toolCallId = generateId();
sendMessage('tool-call', 'start', '', {
func,
args,
});
const result = executeTool(func, args);
sendMessage('tool-call', 'end', '', {
func,
args,
});
return result;
}
function executeTool(func, args) {
if (func === 'ReadFile') {
return fs.readFileSync(args.filePath, 'utf-8');
} else if (func === 'WriteFile') {
fs.writeFileSync(args.filePath, args.content, 'utf-8');
return 'File written successfully';
}
throw new Error(`Unknown tool: ${func}`);
}错误处理
当发生错误时,发送 error 消息:
javascript
function sendError(errorMessage) {
sendMessage('error', 'error', errorMessage);
}会话记忆
CLI Agent 的消息通信协议仅支持单次请求-响应模式(one round-trip)。用户发送一条消息,Agent 处理消息并返回响应,然后退出,agent-server 断开 WebSocket 连接。每一条消息可以视为一个独立的任务。
如果用户希望就同一个主题进行多次交互,客户端可以再次发起 WebSocket 连接并发送新的用户消息。为了保持会话的连续性,需要保证 sessionId 与上次对话一致。
Agent 可以根据 sessionId 来加载对应的会话记忆或会话上下文:
javascript
const fs = require('fs');
const path = require('path');
function loadSessionMemory(sessionId) {
const memoryFile = path.join('/data', '.session', `${sessionId}.json`);
try {
if (fs.existsSync(memoryFile)) {
const data = fs.readFileSync(memoryFile, 'utf-8');
return JSON.parse(data);
}
} catch (error) {
console.error(`Failed to load session memory: ${error.message}`);
}
return { messages: [] };
}
function saveSessionMemory(sessionId, memory) {
const memoryDir = path.join('/data', '.session');
if (!fs.existsSync(memoryDir)) {
fs.mkdirSync(memoryDir, { recursive: true });
}
const memoryFile = path.join(memoryDir, `${sessionId}.json`);
fs.writeFileSync(memoryFile, JSON.stringify(memory, null, 2), 'utf-8');
}
rl.on('line', async (line) => {
try {
const message = JSON.parse(line);
if (message.type === 'user-input') {
const sessionId = message.sessionId;
const memory = loadSessionMemory(sessionId);
memory.messages.push({
role: 'user',
content: message.userInput
});
await processUserInput(message.userInput, memory);
memory.messages.push({
role: 'assistant',
content: lastResponse
});
saveSessionMemory(sessionId, memory);
}
} catch (error) {
sendError(`Failed to process message: ${error.message}`);
}
});发布流程
1. 导出 Docker 镜像
构建完成后,将 Docker 镜像导出为 tar 包:
bash
docker save my-cli-agent:latest -o my-cli-agent.tar注意:不需要使用 gzip 压缩,因为 Docker 导出的 tar 包内部的资源已经经过压缩,再次压缩不会减小体积,反而会浪费 CPU 资源。
2. 提交给管理员
将以下内容提交给 agent-server 平台管理员:
- Docker 镜像文件:
my-cli-agent.tar - 配置信息:yaml
id: my-cli-agent name: "我的CLI Agent" proto: MAP-V1 image: my-cli-agent:latest command: node args: - index.js
3. 管理员安装
管理员执行以下步骤安装 CLI Agent:
导入 Docker 镜像:
bashdocker load -i my-cli-agent.tar编辑配置文件: 在
$CWD/etc/docker.yaml的builtin_agents部分添加配置
配置修改后会立即生效,无需重启服务。agent-server 在处理每个请求时都会重新读取 etc/docker.yaml 配置文件。
4. 用户使用
安装完成后,用户刷新前端页面即可在 Agent 列表中看到新安装的 CLI Agent。
最佳实践
1. 日志记录
将重要信息输出到 stderr,便于调试:
javascript
console.error(`[${new Date().toISOString()}] Processing user input: ${userInput}`);stderr 的内容会被写入日志文件,不会影响 stdout 的消息协议。
2. 错误处理
妥善处理所有可能的错误,并发送 error 消息:
javascript
try {
await processUserInput(message.userInput);
} catch (error) {
console.error('Error processing user input:', error);
sendError(error.message);
sendMessage('response-state', 'end');
}3. 资源清理
在 Agent 退出前清理资源:
javascript
process.on('SIGTERM', () => {
console.error('Received SIGTERM, cleaning up...');
cleanup();
process.exit(0);
});4. 超时处理
agent-server 默认设置了 30 分钟的超时时间。如果你的 Agent 需要更长时间运行,可以考虑:
- 将任务拆分为多个步骤
- 实现断点续传机制
- 在消息中提示用户进度
5. 镜像优化
优化 Docker 镜像大小:
dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production && \
npm cache clean --force
COPY . .
CMD ["node", "index.js"]调试技巧
本地调试
使用管道模拟 stdin/stdout 通信:
bash
echo '{"type":"user-input","userInput":"测试消息"}' | node index.js查看日志
查看 Agent 运行日志:
bash
docker logs agent-server进入容器调试
进入运行中的容器进行调试:
bash
docker exec -it <container-id> sh常见问题
Q: 如何获取用户的 API Key?
A: 用户在首次发送消息时会包含 apiKey 字段。你可以使用这个 API Key 调用 WDS API。
Q: 如何访问沙箱外的文件?
A: CLI Agent 只能访问 /data 目录下的沙箱文件。如果需要访问其他资源,需要通过环境变量或配置文件传递。
Q: Agent 运行时间有限制吗?
A: agent-server 默认设置了 30 分钟的超时时间。超时后容器会被强制停止。
Q: 如何实现多轮对话?
A: 在 Agent 内部维护对话历史,每次收到 user-input 消息时更新历史记录。
相关文档
- Agent 消息协议 - 详细了解 CLI Agent 与前端之间的通信协议
- 开发外部 AI Agent - 学习如何开发一个提供 WebSocket 接口的外部 Agent