--- outline: deep --- # 开发 CLI Agent 本文档将指导你如何开发一个 CLI Agent,并通过 Docker 容器的方式接入 Mythra Agent Studio。 ## 概述 CLI Agent 是一种以命令行工具形式发布的 AI Agent 程序。它具有以下特点: - **封装在 Docker image 中**: CLI Agent 打包成 Docker 镜像,便于分发和部署 - **通过 stdio 通信**: 使用标准输入(stdin)接收消息,标准输出(stdout)发送响应 - **遵循消息协议**: 必须遵循 [Agent 消息协议](./agent-message-protocol.md) 与前端进行通信 - **运行在沙箱环境**: CLI Agent 在沙箱(sandbox)环境中运行,可以访问沙箱文件系统 ## 架构说明 ### 整体架构 ```mermaid sequenceDiagram participant User as 用户 participant Frontend as 前端Web UI participant Server as agent-server participant Docker as Docker容器 participant Agent as CLI Agent User->>Frontend: 发送聊天消息 Frontend->>Server: WebSocket连接 Server->>Docker: docker run --rm -i Docker->>Agent: 启动Agent进程 Server->>Agent: stdin: 用户消息 Agent->>Agent: 处理消息 Agent->>Server: stdout: 响应消息 Server->>Frontend: WebSocket: 响应消息 Frontend->>User: 显示响应 ``` ### 通信流程 1. **发现 Agent**: 前端通过 `/agent/:project_id/list` 接口获取可用的 Agent 列表,其中包含 CLI Agent 2. **建立连接**: 用户选择 CLI Agent 后,前端与 `agent-server` 建立 WebSocket 连接 3. **启动容器**: `agent-server` 使用 `docker run --rm -i` 命令启动 Docker 容器 4. **消息传递**: - WebSocket 上行消息 → CLI Agent 的 stdin - CLI Agent 的 stdout → WebSocket 下行消息 - CLI Agent 的 stderr → 日志文件 5. **结束会话**: 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 openai ``` ### 3. 实现消息处理 创建 `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:/data ``` Agent 可以在 `/data` 目录中读写文件,这些文件会同步到用户的沙箱环境中。 ## 消息协议 CLI Agent 必须遵循 [Agent 消息协议](./agent-message-protocol.md) 进行通信。以下是关键要点: ### 输入消息格式 从 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` 平台管理员: 1. **Docker 镜像文件**: `my-cli-agent.tar` 2. **配置信息**: ```yaml id: my-cli-agent name: "我的CLI Agent" proto: MAP-V1 image: my-cli-agent:latest command: node args: - index.js ``` ### 3. 管理员安装 管理员执行以下步骤安装 CLI Agent: 1. **导入 Docker 镜像**: ```bash docker load -i my-cli-agent.tar ``` 2. **编辑配置文件**: 在 `$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 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 消息协议](./agent-message-protocol.md) - 详细了解 CLI Agent 与前端之间的通信协议 - [开发外部 AI Agent](./develop-external-agent.md) - 学习如何开发一个提供 WebSocket 接口的外部 Agent