# Agent 工作台 - 技术方案文档 > 版本: v0.3 | 更新时间: 2026-05-20 > 关联文档: [requirement.md](./requirement.md) > **文档状态(2026-07)**:本文档为早期技术方案,部分模块已变更。产品说明、部署与使用手册以 [skills/mao-cli/SKILL.md](../skills/mao-cli/SKILL.md) 为准;架构概览亦见 [project.md](../skills/mao-cli/reference/project.md)。源码与下列差异: > - **Hub 模块**已删除(`V017__remove_hub.sql`) > - 前端流式通道为 **WebSocket**(`StreamingWsHandler`),LLM 内部仍通过 OkHttp SSE 拉流;**0.0.82 起 WS 改为首帧鉴权**(`auth` 帧携带 token,URL 不再传 token) > - 对象存储支持 **本地文件系统** 与 **阿里云 OSS** > - **集成配置(LDAP/飞书登录/上传/Tavily·TinyFish 搜索/OSS)0.0.82 起迁入 `system_setting` 表**,管理后台可视化编辑、热生效;新增 `settings:read`/`settings:write` 权限与 `SETTINGS_SECRET` 加密主密钥 > - Flyway 迁移脚本现为 **46 个**(非文档初稿中的 24 个) > - 开源协议:**MIT**,仅提供自部署 --- ## 1. 技术选型明细 ### 1.1 后端技术栈 | 组件 | 选型 | 版本建议 | 选型理由 | |------|------|---------|---------| | 语言 | Java | 17+ (LTS) | 虚拟线程支持,企业级生态成熟 | | 框架 | Spring Boot | 3.x | 自动配置、生态丰富、社区活跃 | | Web 框架 | **Spring MVC** | - | 团队熟悉,SSE 通过 `SseEmitter` 实现 | | ORM | MyBatis-Plus | 3.5.x | 灵活的 SQL 控制,适合复杂查询 | | 认证 | Spring Security | - | LDAP / OAuth2 开箱即用 | | HTTP 客户端 | OkHttp / Apache HttpClient | - | LLM API 调用,支持 SSE 流式读取 | | 对象存储 | MinIO (S3 兼容) | - | 私有化部署,S3 协议标准 | | LLM 客户端 | 自研 OpenAI 协议适配层 | - | 统一协议,流式 SSE 解析 | | MCP 客户端 | 自研 MCP SDK (Java) | - | MCP 协议规范实现 | | API 文档 | SpringDoc (OpenAPI 3) | 2.x | 自动生成 API 文档 | | 工具库 | Hutool / Guava | - | 常用工具方法 | ### 1.2 前端技术栈 | 组件 | 选型 | 版本建议 | 选型理由 | |------|------|---------|---------| | 框架 | Vue 3 | 3.4+ | Composition API、更好的 TS 支持 | | 构建工具 | Vite | 5.x | 快速构建、HMR | | 状态管理 | Pinia | 2.x | Vue 3 官方推荐,轻量 | | UI 组件库 | **Element Plus** | 2.x | 企业级 UI 组件丰富,中文文档完善 | | HTTP 客户端 | Axios | 1.x | 拦截器机制适合 Token 注入 | | 路由 | Vue Router | 4.x | Vue 3 配套 | | 国际化 | Vue I18n | 9.x | 多语言支持(可选) | | Markdown 渲染 | markdown-it | - | Agent 对话内容渲染 | | 代码高亮 | highlight.js / Shiki | - | 代码块语法高亮 | ### 1.3 桌面客户端技术栈 | 组件 | 选型 | 版本建议 | 选型理由 | |------|------|---------|---------| | 框架 | Electron | 30+ | 跨平台成熟方案 | | 前端 | Vue 3 + Vite | - | 独立工程,与管理后台独立维护 | | 窗口管理 | electron-window-state | - | 窗口位置/大小记忆 | | 自动更新 | electron-updater | - | 应用内自动更新 | | 安全 | contextIsolation + sandbox | - | Electron 安全最佳实践 | | IPC 通信 | electron IPC + 预加载脚本 | - | 主进程与渲染进程安全通信 | ### 1.4 基础设施 | 组件 | 选型 | 备注 | |------|------|------| | 数据库 | MySQL 8.x | 主数据存储 | | 对象存储 | MinIO | 文件存储(私有化) | | 反向代理 | Nginx | 负载均衡 + HTTPS 终结 | | 日志 | SLF4J + Logback | 应用日志,可选接 ELK | --- ## 2. 工程结构 ### 2.1 Monorepo 目录规划 ``` mao/ ├── backend/ # 后端 Java 服务 │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/cn/etarch/mao/ │ │ ├── WorkbenchApplication.java │ │ ├── config/ # 配置类 │ │ │ ├── SecurityConfig.java │ │ │ ├── WebMvcConfig.java │ │ │ └── LlmConfig.java │ │ ├── common/ # 公共模块 │ │ │ ├── result/ # 统一响应 │ │ │ ├── exception/ # 全局异常 │ │ │ ├── constant/ # 常量 │ │ │ └── util/ │ │ ├── auth/ # 认证模块 │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── ldap/ # LDAP 集成 │ │ │ └── feishu/ # 飞书 OAuth │ │ ├── user/ # 用户模块 │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── mapper/ │ │ │ └── entity/ │ │ ├── agent/ # Agent 管理模块 │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── mapper/ │ │ │ └── entity/ │ │ ├── harness/ # Agent Harness 核心 │ │ │ ├── core/ # 核心引擎 │ │ │ │ ├── AgentLoop.java │ │ │ │ ├── PromptEngine.java │ │ │ │ ├── ContextManager.java │ │ │ │ └── ExecutionPipeline.java │ │ │ ├── llm/ # LLM 适配层 │ │ │ │ ├── LlmAdapter.java │ │ │ │ ├── LlmClient.java │ │ │ │ ├── OpenAiProtocol.java │ │ │ │ └── model/ │ │ │ ├── skill/ # Skills 体系 │ │ │ │ ├── SkillRegistry.java │ │ │ │ ├── SkillDispatcher.java │ │ │ │ ├── SkillExecutor.java │ │ │ │ └── builtin/ # 内置 Skills │ │ │ ├── mcp/ # MCP 协议 │ │ │ │ ├── McpClient.java │ │ │ │ ├── McpServerManager.java │ │ │ │ └── McpToolAdapter.java │ │ │ └── safety/ # 安全沙箱 │ │ │ ├── SafetyGuard.java │ │ │ └── PermissionChecker.java │ │ ├── session/ # 会话模块 │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── mapper/ │ │ │ └── entity/ │ │ ├── hub/ # Hub 模块 │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ └── entity/ │ │ ├── audit/ # 审计模块 │ │ │ ├── interceptor/ # 审计拦截器 │ │ │ ├── service/ │ │ │ └── entity/ │ │ └── file/ # 文件模块 │ │ ├── controller/ │ │ └── service/ │ └── resources/ │ ├── application.yml │ ├── application-dev.yml │ └── db/migration/ # 数据库迁移脚本 │ ├── admin/ # 管理后台 (Vue 3) │ ├── package.json │ ├── vite.config.ts │ └── src/ │ ├── main.ts │ ├── App.vue │ ├── router/ │ ├── stores/ # Pinia stores │ ├── views/ │ │ ├── auth/ # 登录 │ │ ├── user/ # 用户管理 │ │ ├── model/ # 模型管理 │ │ ├── agent/ # Agent 管理 │ │ ├── skill/ # Skills 管理 │ │ ├── hub/ # Hub 管理 │ │ ├── audit/ # 审计日志 │ │ └── system/ # 系统配置 │ ├── components/ │ ├── composables/ │ ├── api/ # API 接口层 │ └── utils/ │ ├── desktop/ # 桌面客户端 (Electron + Vue 3) │ ├── package.json │ ├── electron/ │ │ ├── main.ts # Electron 主进程 │ │ ├── preload.ts # 预加载脚本 │ │ └── ipc/ # IPC 处理 │ ├── src/ # Vue 3 渲染进程 │ │ ├── main.ts │ │ ├── App.vue │ │ ├── router/ │ │ ├── stores/ │ │ ├── views/ │ │ │ ├── auth/ # 登录 │ │ │ ├── workbench/ # 工作台主页 │ │ │ ├── chat/ # Agent 对话 │ │ │ ├── hub/ # Agent Hub │ │ │ ├── agent-create/ # 创建 Agent │ │ │ └── settings/ # 设置 │ │ ├── components/ │ │ │ ├── chat/ # 对话组件 │ │ │ ├── agent/ # Agent 相关组件 │ │ │ └── common/ │ │ ├── composables/ │ │ ├── api/ │ │ └── utils/ │ └── resources/ # 应用图标等资源 │ └── docs/ # 文档 ├── requirement.md └── 2026-05-20-technical-design.md ``` --- ## 3. 数据库设计 ### 3.1 核心表结构 ```sql -- ========== 用户与权限 ========== CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(64) NOT NULL UNIQUE, `display_name` VARCHAR(128) NOT NULL, `email` VARCHAR(128), `avatar_url` VARCHAR(512), `auth_type` VARCHAR(20) NOT NULL COMMENT 'LDAP / FEISHU', `password_hash` VARCHAR(128) COMMENT 'LDAP 用户可为空', `feishu_user_id` VARCHAR(64) COMMENT '飞书用户 ID', `department_id` BIGINT, `status` TINYINT DEFAULT 1 COMMENT '1-启用 0-禁用', `last_login_at` DATETIME, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `role` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL UNIQUE COMMENT '角色名称', `code` VARCHAR(64) NOT NULL UNIQUE COMMENT '角色编码', `description` VARCHAR(256), `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_role` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `role_id` BIGINT NOT NULL, UNIQUE KEY `uk_user_role` (`user_id`, `role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `permission` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(128) NOT NULL, `code` VARCHAR(128) NOT NULL UNIQUE COMMENT '权限编码,如 agent:read, agent:write', `description` VARCHAR(256), `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `role_permission` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `role_id` BIGINT NOT NULL, `permission_id` BIGINT NOT NULL, UNIQUE KEY `uk_role_perm` (`role_id`, `permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `department` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(128) NOT NULL, `parent_id` BIGINT DEFAULT 0, `sort_order` INT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ========== AI 模型配置 ========== CREATE TABLE `llm_model` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(128) NOT NULL COMMENT '模型显示名称', `provider` VARCHAR(64) COMMENT '供应商标识', `base_url` VARCHAR(512) NOT NULL COMMENT 'API 地址', `api_key` VARCHAR(512) NOT NULL COMMENT '加密存储的 API Key', `model_id` VARCHAR(128) NOT NULL COMMENT '模型标识,如 gpt-4o', `max_tokens` INT DEFAULT 4096, `temperature_max` DECIMAL(3,2) DEFAULT 2.00, `status` TINYINT DEFAULT 1 COMMENT '1-启用 0-禁用', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ========== Agent ========== CREATE TABLE `agent` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(128) NOT NULL, `description` TEXT, `icon_url` VARCHAR(512), `system_prompt` TEXT NOT NULL COMMENT '系统提示词(人格设定)', `model_id` BIGINT NOT NULL COMMENT '关联的 LLM 模型', `creator_id` BIGINT NOT NULL COMMENT '创建者用户 ID', `type` VARCHAR(20) NOT NULL COMMENT 'SYSTEM-系统级 PERSONAL-个人', `visibility` VARCHAR(20) DEFAULT 'PRIVATE' COMMENT 'PRIVATE / HUB / SPECIFIC', `status` VARCHAR(20) DEFAULT 'DRAFT' COMMENT 'DRAFT/PUBLISHED/ARCHIVED', `token_limit` INT DEFAULT 0 COMMENT '单次对话 Token 限额,0-不限', `max_rounds` INT DEFAULT 0 COMMENT '单次对话最大轮数,0-不限', `config_json` JSON COMMENT '扩展配置', `published_at` DATETIME COMMENT '发布到 Hub 的时间', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `agent_tag` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `agent_id` BIGINT NOT NULL, `tag` VARCHAR(64) NOT NULL, INDEX `idx_agent_tag` (`agent_id`, `tag`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `agent_skill` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `agent_id` BIGINT NOT NULL, `skill_id` BIGINT NOT NULL COMMENT '关联 skill 表', `config` JSON COMMENT '该 Agent 对此 Skill 的定制配置', UNIQUE KEY `uk_agent_skill` (`agent_id`, `skill_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `agent_mcp_config` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `agent_id` BIGINT NOT NULL, `server_url` VARCHAR(512) NOT NULL COMMENT 'MCP Server 地址', `transport` VARCHAR(20) DEFAULT 'SSE' COMMENT 'SSE / STDIO', `config` JSON COMMENT '额外配置', `status` TINYINT DEFAULT 1, INDEX `idx_agent` (`agent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `agent_permission` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `agent_id` BIGINT NOT NULL, `permission_type` VARCHAR(20) NOT NULL COMMENT 'ROLE / DEPARTMENT / USER', `permission_value` BIGINT NOT NULL COMMENT '对应 role_id / department_id / user_id', UNIQUE KEY `uk_agent_perm` (`agent_id`, `permission_type`, `permission_value`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ========== Skills ========== CREATE TABLE `skill` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(128) NOT NULL, `description` TEXT, `type` VARCHAR(20) NOT NULL COMMENT 'BUILTIN-内置', `input_schema` JSON COMMENT '输入参数 JSON Schema', `output_schema` JSON COMMENT '输出参数 JSON Schema', `impl_class` VARCHAR(256) COMMENT '内置 Skill 的 Java 实现类全限定名', `creator_id` BIGINT, `status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT 'ACTIVE/DISABLED', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ========== 会话与消息 ========== CREATE TABLE `session` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `agent_id` BIGINT NOT NULL, `title` VARCHAR(256) COMMENT '会话标题,可自动生成', `status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT 'ACTIVE/ARCHIVED', `is_pinned` TINYINT DEFAULT 0, `is_favorite` TINYINT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_user` (`user_id`), INDEX `idx_agent` (`agent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `message` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `session_id` BIGINT NOT NULL, `role` VARCHAR(20) NOT NULL COMMENT 'USER / ASSISTANT / SYSTEM / TOOL', `content` MEDIUMTEXT COMMENT '消息内容', `tool_call_id` VARCHAR(128) COMMENT '工具调用 ID(tool role 消息时使用)', `tool_calls` JSON COMMENT '工具调用列表(assistant 消息中的 function_call)', `token_count` INT DEFAULT 0 COMMENT '本条消息的 Token 消耗', `model_id` BIGINT COMMENT '使用的模型', `metadata` JSON COMMENT '扩展元数据', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_session` (`session_id`), INDEX `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ========== Hub ========== CREATE TABLE `hub_installation` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `agent_id` BIGINT NOT NULL COMMENT 'Hub 中的 Agent', `installed_at` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_user_agent` (`user_id`, `agent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ========== 审计日志 ========== CREATE TABLE `audit_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT, `username` VARCHAR(64), `action` VARCHAR(64) NOT NULL COMMENT '操作类型', `resource_type` VARCHAR(64) COMMENT '资源类型', `resource_id` VARCHAR(64) COMMENT '资源 ID', `detail` JSON COMMENT '操作详情', `ip` VARCHAR(45), `user_agent` VARCHAR(512), `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_user` (`user_id`), INDEX `idx_action` (`action`), INDEX `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `api_call_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT, `session_id` BIGINT, `agent_id` BIGINT, `endpoint` VARCHAR(256) NOT NULL, `method` VARCHAR(10), `request_body` MEDIUMTEXT, `response_code` INT, `latency_ms` INT, `llm_model` VARCHAR(128), `llm_tokens_in` INT DEFAULT 0, `llm_tokens_out` INT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_session` (`session_id`), INDEX `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ## 4. API 设计 ### 4.1 API 总览 | 模块 | 方法 | 路径 | 说明 | |------|------|------|------| | **认证** | POST | `/api/v1/auth/login` | 统一登录入口 | | | POST | `/api/v1/auth/feishu/qrcode` | 获取飞书扫码二维码 | | | POST | `/api/v1/auth/feishu/callback` | 飞书扫码回调 | | | POST | `/api/v1/auth/refresh` | 刷新 Token | | | POST | `/api/v1/auth/logout` | 退出登录 | | **用户** | GET | `/api/v1/users/me` | 获取当前用户信息 | | | GET | `/api/v1/users` | 用户列表(管理员) | | | PUT | `/api/v1/users/{id}/status` | 启用/禁用用户 | | **模型** | GET | `/api/v1/models` | 模型列表 | | | POST | `/api/v1/models` | 创建模型配置 | | | PUT | `/api/v1/models/{id}` | 更新模型配置 | | | POST | `/api/v1/models/{id}/test` | 测试模型连通性 | | **Agent** | GET | `/api/v1/agents` | 用户可用的 Agent 列表 | | | GET | `/api/v1/agents/{id}` | Agent 详情 | | | POST | `/api/v1/agents` | 创建 Agent | | | PUT | `/api/v1/agents/{id}` | 更新 Agent | | | DELETE | `/api/v1/agents/{id}` | 删除 Agent | | | POST | `/api/v1/agents/{id}/publish` | 发布到 Hub | | **会话** | POST | `/api/v1/sessions` | 创建会话 | | | GET | `/api/v1/sessions` | 会话列表 | | | GET | `/api/v1/sessions/{id}` | 会话详情 | | | DELETE | `/api/v1/sessions/{id}` | 删除会话 | | | PUT | `/api/v1/sessions/{id}/pin` | 置顶/取消置顶 | | **对话** | POST | `/api/v1/sessions/{id}/messages` | 发送消息(触发 Agent) | | | GET | `/api/v1/sessions/{id}/messages` | 历史消息列表 | | | GET | `/api/v1/sessions/{id}/stream` | SSE 流式输出 | | **Hub** | GET | `/api/v1/hub/agents` | Hub Agent 列表 | | | POST | `/api/v1/hub/agents/{id}/install` | 安装 Agent | | | DELETE | `/api/v1/hub/agents/{id}/install` | 卸载 Agent | | **Skills** | GET | `/api/v1/skills` | Skills 列表 | | | POST | `/api/v1/skills` | 创建/上传 Skill | | | PUT | `/api/v1/skills/{id}` | 更新 Skill | | **文件** | POST | `/api/v1/files/upload` | 上传文件 | | | GET | `/api/v1/files/{id}/download` | 下载文件 | | **审计** | GET | `/api/v1/audit/logs` | 审计日志查询 | | | GET | `/api/v1/audit/api-logs` | API 调用日志查询 | ### 4.2 统一响应格式 ```json { "code": 0, "message": "success", "data": { ... }, "timestamp": 1716105600000 } ``` 错误码规范: | 范围 | 含义 | |------|------| | 0 | 成功 | | 1001-1999 | 认证/授权错误 | | 2001-2999 | 参数校验错误 | | 3001-3999 | 业务逻辑错误 | | 5001-5999 | 服务端内部错误 | ### 4.3 SSE 流式输出协议 对话采用 SSE(Server-Sent Events)实现流式输出: ``` GET /api/v1/sessions/{id}/stream Authorization: Bearer Accept: text/event-stream # 事件类型 event: message_start data: {"message_id": "msg_123", "model": "gpt-4o"} event: content_delta data: {"delta": "你好"} event: content_delta data: {"delta": ",有什么"} event: tool_call_start data: {"tool_call_id": "tc_456", "tool_name": "web_search", "arguments": "{"query":"..."}"} event: tool_call_result data: {"tool_call_id": "tc_456", "result": "..."} event: message_end data: {"message_id": "msg_123", "usage": {"prompt_tokens": 120, "completion_tokens": 85}} event: error data: {"code": 5001, "message": "LLM 调用超时"} ``` --- ## 5. 技术难点与解决方案 ### 5.1 Agent Harness 核心引擎设计 **难点:** Harness 是整个系统的核心,需要统一处理不同 Agent 的人格、工具、上下文,同时保持可扩展性。 **方案:** 采用 Pipeline + 插件化架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ Execution Pipeline │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│ │ │ Prompt │──▶│ LLM │──▶│ Response │──▶│ Output ││ │ │ Compose │ │ Call │ │ Parse │ │ Render ││ │ └──────────┘ └──────────┘ └────┬─────┘ └──────────┘│ │ │ │ │ ┌──────▼──────┐ │ │ │ Tool Call? │ │ │ └──────┬──────┘ │ │ Yes │ No → Output │ │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Tool Dispatch Layer │ │ │ │ ┌────────────┐ ┌────────────┐ │ │ │ │ │ Skill │ │ MCP │ │ │ │ │ │ Executor │ │ Client │ │ │ │ │ └────────────┘ └────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Result Feedback │ │ (回到 LLM Call) │ └─────────────────────────────────────────────────────────────┘ ``` **关键设计决策:** 1. **Agent Loop 作为核心循环**:借鉴 Codex/ChatGPT 的 Agent Loop 模式,LLM 输出 → 解析 → 执行工具 → 结果反馈 → 再次 LLM 调用,直到 LLM 返回纯文本结果或达到最大轮次。 2. **统一的 Tool 接口**:所有工具(内置 Skill、MCP 工具)实现统一的 `Tool` 接口: ```java public interface Tool { String getName(); String getDescription(); JsonSchema getInputSchema(); ToolResult execute(JsonObject arguments, ExecutionContext context); } ``` 3. **异步执行 + 流式反馈**:工具执行异步化,执行过程中通过 SSE 实时推送进度给客户端。 ### 5.2 LLM 统一协议适配 **难点:** 需要对接不同厂商的 LLM 服务,虽然都声称兼容 OpenAI 协议,但实际上各家有细微差异。 **方案:** 三层适配架构 ``` ┌───────────────────────────────────────┐ │ LlmAdapter (接口层) │ │ 定义统一的调用接口:chat() / stream() │ └───────────────┬───────────────────────┘ │ ┌───────────────▼───────────────────────┐ │ OpenAiProtocol (协议层) │ │ OpenAI 兼容格式的请求/响应构建与解析 │ │ - ChatCompletion 请求构建 │ │ - SSE 流式响应解析 │ │ - function_call / tool_calls 解析 │ └───────────────┬───────────────────────┘ │ ┌───────────────▼───────────────────────┐ │ ProviderAdapter (供应商适配层) │ │ 处理各厂商的协议差异: │ │ - 请求头差异(认证方式) │ │ - 响应格式微调(字段映射) │ │ - 特有参数(如 Claude 的 system) │ └───────────────────────────────────────┘ ``` 核心类设计: ```java // 统一适配接口 public interface LlmAdapter { ChatResponse chat(ChatRequest request, LlmModelConfig config); void stream(ChatRequest request, LlmModelConfig config, StreamCallback callback); } // Stream 回调接口 public interface StreamCallback { void onChunk(StreamChunk chunk); void onComplete(ChatUsage usage); void onError(Throwable t); } // OpenAI 协议实现(默认,基于 OkHttp) public class OpenAiLlmAdapter implements LlmAdapter { private final OkHttpClient httpClient; @Override public void stream(ChatRequest request, LlmModelConfig config, StreamCallback callback) { RequestBody body = RequestBody.create( buildRequestBody(request), MediaType.parse("application/json")); Request httpRequest = new Request.Builder() .url(config.getBaseUrl() + "/chat/completions") .header("Authorization", "Bearer " + config.getApiKey()) .post(body) .build(); try (Response response = httpClient.newCall(httpRequest).execute()) { BufferedSource source = response.body().source(); while (!source.exhausted()) { String line = source.readUtf8Line(); if (line != null && line.startsWith("data: ")) { StreamChunk chunk = parseStreamChunk(line.substring(6)); callback.onChunk(chunk); } } callback.onComplete(extractUsage(response)); } catch (Exception e) { callback.onError(e); } } } ``` ### 5.3 MCP 协议对接 **难点:** MCP (Model Context Protocol) 是 Anthropic 提出的开放协议,Java 生态缺乏成熟 SDK,需要自研。MCP 支持 SSE 和 STDIO 两种传输方式。 **方案:** 自研轻量 MCP Java Client ``` ┌─────────────────────────────────────────┐ │ MCP Client │ │ │ │ ┌─────────────────────────────────┐ │ │ │ McpServerManager │ │ │ │ - 服务器连接池管理 │ │ │ │ - 健康检查 / 自动重连 │ │ │ └────────────┬────────────────────┘ │ │ │ │ │ ┌────────────▼────────────────────┐ │ │ │ Transport Layer │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ │ │ SSE │ │ STDIO │ │ │ │ │ │ Transport │ │ Transport│ │ │ │ │ └──────────┘ └──────────┘ │ │ │ └────────────┬────────────────────┘ │ │ │ │ │ ┌────────────▼────────────────────┐ │ │ │ Protocol Layer │ │ │ │ - initialize / initialized │ │ │ │ - tools/list │ │ │ │ - tools/call │ │ │ │ - resources/list │ │ │ │ - resources/read │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` **核心流程:** 1. **连接阶段**:Agent 初始化时,根据 `agent_mcp_config` 表中的配置,建立与 MCP Server 的连接 2. **工具发现**:调用 `tools/list` 获取该 MCP Server 提供的所有工具,自动转换为 Harness 内部的 `Tool` 接口 3. **工具调用**:当 LLM 返回 tool_call 且目标工具属于 MCP 时,通过 MCP 协议转发调用 4. **生命周期管理**:连接池管理、心跳检测、断线重连 ### 5.4 用户上传 Skills(暂缓) **当前策略:** MVP 阶段仅支持管理后台注册的**内置 Skills** + **MCP Server 工具**两种来源。用户上传自定义脚本的能力预留设计,Phase 2 再实现。 **预留设计:** - `skill` 表保留 `type` 字段,未来扩展 `USER_UPLOAD` 类型 - `Tool` 接口设计已考虑动态加载,未来可增加 `ScriptTool` 实现类 - Phase 2 实现时需引入 Docker 沙箱 + 代码审核流程 ### 5.5 Agent 对话的流式输出 **难点:** 整个链路(客户端 → 后端 → LLM → 后端 → 客户端)都需要保持流式,任何一环阻塞都会影响体验。 **方案:** 全链路 SSE 流式管道(Spring MVC + SseEmitter) ``` 客户端 (Electron) 服务端 (Spring MVC) LLM API │ │ │ │ POST /messages │ │ │ ──────────────────────▶ │ │ │ │ POST /chat/completions │ │ │ (stream: true) │ │ │ ────────────────────────▶ │ │ │ │ │ GET /stream (SSE) │ ◀── SSE chunks ────────── │ │ ──────────────────────▶ │ │ │ │ │ │ ◀── content_delta ───── │ ◀── data: {...} ───────── │ │ ◀── content_delta ───── │ ◀── data: {...} ───────── │ │ ◀── tool_call_start ─── │ ◀── data: {...} ───────── │ │ ◀── tool_call_result ── │ (内部执行工具) │ │ ◀── content_delta ───── │ ◀── 第二轮 LLM 调用 ────── │ │ ◀── message_end ─────── │ │ │ │ │ ``` **技术实现要点:** 1. **后端使用 Spring MVC + `SseEmitter`**:Servlet 异步机制实现 SSE,无需切换到 WebFlux 2. **LLM 调用使用 OkHttp**:通过 `ResponseBody.source()` 逐行读取 SSE 流,回调推送到 `SseEmitter` 3. **客户端使用 EventSource API**:Electron 渲染进程中标准的 SSE 消费方式 4. **Agent Loop 运行在独立线程池**:避免阻塞 Servlet 线程,通过 `SseEmitter` 跨线程推送事件 ```java // 后端核心流式处理 @GetMapping(value = "/api/v1/sessions/{sessionId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(@PathVariable Long sessionId, @RequestParam String eventId) { SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); // 5 分钟超时 // 提交到独立线程池执行 Agent Loop agentExecutor.submit(() -> { try { harnessService.execute(sessionId, eventId, new AgentEventListener() { @Override public void onContentDelta(String delta) { emitter.send(SseEmitter.event() .name("content_delta") .data(Map.of("delta", delta))); } @Override public void onToolCallStart(ToolCall call) { emitter.send(SseEmitter.event() .name("tool_call_start") .data(call)); } @Override public void onMessageEnd(Usage usage) { emitter.send(SseEmitter.event() .name("message_end") .data(usage)); emitter.complete(); } @Override public void onError(Throwable t) { emitter.send(SseEmitter.event() .name("error") .data(Map.of("message", t.getMessage()))); emitter.completeWithError(t); } }); } catch (Exception e) { emitter.completeWithError(e); } }); emitter.onTimeout(() -> emitter.complete()); emitter.onError(e -> log.warn("SSE connection error", e)); return emitter; } // Harness 内部 - 使用 OkHttp 读取 LLM SSE 流 public void execute(Long sessionId, String eventId, AgentEventListener listener) { // 1. 加载 Agent 配置 + 构建 Prompt // 2. 进入 Agent Loop AgentContext context = buildContext(sessionId, eventId); while (context.hasNextRound()) { // OkHttp 同步请求,逐行读取 SSE try (Response response = llmClient.stream(context.buildRequest())) { BufferedSource source = response.body().source(); while (!source.exhausted()) { String line = source.readUtf8Line(); StreamChunk chunk = parseSSE(line); switch (chunk.getType()) { case DELTA -> listener.onContentDelta(chunk.getDelta()); case TOOL_CALL -> handleToolCall(chunk, context, listener); } } } // 如果有工具调用,执行后继续下一轮 if (context.hasPendingToolCalls()) { executeToolCalls(context, listener); } else { break; // 纯文本回复,结束循环 } } listener.onMessageEnd(context.getUsage()); } ``` ### 5.6 上下文窗口管理 **难点:** LLM 有 Token 上下文限制,长对话会超出窗口。需要智能管理上下文,保证对话连贯性。 **方案:** 三级上下文策略 ``` ┌─────────────────────────────────────────────────────┐ │ Context Manager │ │ │ │ Level 1: 系统提示词(固定,始终保留) │ │ ┌─────────────────────────────────────────────┐ │ │ │ System Prompt + Agent 人格 + 工具描述 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ Level 2: 近期对话(滑动窗口,优先保留) │ │ ┌─────────────────────────────────────────────┐ │ │ │ 最近 N 轮完整对话(user + assistant + tool) │ │ │ │ N 由 max_context_rounds 配置决定 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ Level 3: 历史摘要(压缩旧对话) │ │ ┌─────────────────────────────────────────────┐ │ │ │ 对超出窗口的旧对话生成摘要,作为 context 注入 │ │ │ │ 摘要可由 LLM 自动生成或规则截断 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Token 预算分配: │ │ │ │ 系统提示词: ~15% │ │ │ │ 工具描述: ~10% │ │ │ │ 历史摘要: ~10% │ │ │ │ 近期对话: ~50% │ │ │ │ 预留输出: ~15% │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ ``` **摘要生成策略:** - 当对话轮数超过 `max_context_rounds` 时,将最早的对话批次送入 LLM 生成摘要 - 摘要格式:`[对话摘要] 用户询问了XX,助手完成了XX,关键结论:XX` - 摘要随对话推进持续更新(增量摘要) ### 5.7 并发控制与限流 **难点:** 多用户并发使用 Agent,LLM API 有速率限制,需要合理控制并发。 **方案:** 多级限流 + 线程池控制 ``` 请求进入 │ ▼ ┌──────────────┐ │ 用户级限流 │ 应用内滑动窗口 │ (每用户 QPS) │ 例:10 次/分钟 └──────┬───────┘ ▼ ┌──────────────┐ │ Agent 级限流 │ 应用内计数器 │ (并发会话数) │ 例:同一 Agent 最多 50 并发 └──────┬───────┘ ▼ ┌──────────────┐ │ 模型级限流 │ 应用内令牌桶 │ (API 调用速率)│ 例:60 次/分钟 └──────┬───────┘ ▼ ┌──────────────┐ │ 线程池调度 │ ThreadPoolExecutor │ (并发控制) │ 核心线程 / 最大线程 / 队列 └──────────────┘ ``` **实现细节:** - 用户级限流:应用内滑动窗口计数 - 模型级限流:读取 `llm_model` 配置中的速率限制参数 - 并发控制:Agent 执行提交到专用线程池,通过有界队列 + CallerRunsPolicy 背压 - 客户端体验:线程池满时返回 `event: queue_position` 告知用户排队位置 - 线程池配置建议:核心线程 20,最大线程 100,队列容量 200 ### 5.8 Electron 安全与性能 **难点:** Electron 应用需要兼顾安全性和性能,避免常见安全漏洞。 **方案:** ``` ┌─────────────────────────────────────────────────────┐ │ Electron 安全架构 │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 主进程 │◀─IPC──▶│ 渲染进程 │ │ │ │ (Node.js) │ │ (Vue 3) │ │ │ │ │ │ │ │ │ │ - 窗口管理 │ │ - UI 渲染 │ │ │ │ - 系统交互 │ │ - 用户交互 │ │ │ │ - 自动更新 │ │ - API 调用 │ │ │ │ - 文件操作 │ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ 安全配置: │ │ - contextIsolation: true │ │ - nodeIntegration: false │ │ - sandbox: true │ │ - preload.ts 白名单暴露 IPC 接口 │ └─────────────────────────────────────────────────────┘ ``` **性能优化:** - Vue 3 组件懒加载 + 路由懒加载 - 对话列表虚拟滚动(大量消息时) - Markdown 渲染结果缓存 - 主进程与渲染进程职责分离,避免阻塞 UI --- ## 6. 安全架构 ### 6.1 认证流程 ``` LDAP 登录: 用户 ──▶ 输入账号密码 ──▶ 后端 ──▶ LDAP Server 验证 │ 成功 ◀───┘ │ 生成 JWT Token │ 返回给客户端 飞书扫码登录: 用户 ──▶ 点击飞书登录 ──▶ 后端生成二维码 URL │ 用户 ◀── 展示二维码 │ │ 用户手机扫码 ──▶ 飞书 ──▶ 回调后端 /auth/feishu/callback │ 验证授权码 ──▶ 获取用户信息 │ 匹配/创建用户 ──▶ 生成 JWT Token │ 返回给客户端 ``` ### 6.2 权限模型 ``` User ──▶ UserRole ──▶ Role ──▶ RolePermission ──▶ Permission │ ┌─────────┼─────────┐ ▼ ▼ ▼ agent:read agent:write ... │ ▼ AgentPermission (Agent 级权限) │ ┌───────────┼───────────┐ ▼ ▼ ▼ ROLE DEPARTMENT USER (角色可见) (部门可见) (指定用户) ``` **权限校验流程:** 1. API 请求进入 → JWT 解析 → 获取用户 ID 2. 查询用户角色 → 获取权限列表 3. 检查是否有对应 API 的权限编码 4. 如果是 Agent 操作,额外检查 AgentPermission(该用户是否有权访问此 Agent) ### 6.3 数据安全 | 数据类型 | 保护措施 | |---------|---------| | 用户密码 | bcrypt 哈希存储 | | API Key | AES-256 加密存储,仅服务端使用 | | 对话内容 | 传输加密 (HTTPS) + 存储访问控制 | | 审计日志 | 仅管理员可查,不可修改/删除 | --- ## 7. 部署架构 ### 7.1 私有化部署拓扑 ``` ┌─────────────────────────────────────────────────────────────┐ │ 企业内网 / 私有云 │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Nginx (反向代理) │ │ │ │ HTTPS 终结 + 负载均衡 │ │ │ └────────────┬──────────────┬──────────────┬────────────┘ │ │ │ │ │ │ │ ┌────────────▼───┐ ┌──────▼──────┐ ┌───▼────────────┐ │ │ │ Backend │ │ Admin │ │ MinIO │ │ │ │ (Spring Boot)│ │ (Vue 3) │ │ (文件存储) │ │ │ │ x N 实例 │ │ 静态资源 │ │ │ │ │ └────────┬───────┘ └─────────────┘ └────────────────┘ │ │ │ │ │ ┌────────▼──────────────────────────────────────────────┐ │ │ │ 内部网络 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ MySQL │ │ LDAP │ │ │ │ │ │ 8.x │ │ Server │ │ │ │ │ └─────────┘ └─────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ 外部依赖: │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ LLM API (OpenAI / Claude / 自部署模型) │ │ │ │ 飞书开放平台 (扫码登录) │ │ │ │ MCP Server (外部工具服务) │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 8. 关键依赖与风险评估 | 风险项 | 影响 | 等级 | 应对措施 | |--------|------|------|---------| | LLM API 稳定性 | 对话功能不可用 | 高 | 多模型备份、降级策略、超时重试 | | MCP 协议演进 | 需要持续适配 | 中 | 抽象协议层,隔离变化影响 | | Electron 包体大小 | 用户下载体验 | 低 | asar 打包 + 按需分发 | | 长对话 Token 超限 | 对话截断 | 中 | 智能上下文管理 + 摘要压缩 | | 并发压力 | 响应变慢 | 中 | 多级限流 + 线程池背压 + 水平扩展 | --- ## 9. 已确认决策 | 问题 | 决策 | |------|------| | Spring WebFlux vs Spring MVC | **Spring MVC** + `SseEmitter` 实现 SSE | | 消息队列选型 | **暂不引入 MQ**,使用线程池控制并发 | | 用户上传 Skills | **MVP 跳过**,仅支持内置 Skills + MCP | | 前端代码共享 | **独立维护**,管理后台与桌面端各自独立 | > 暂无待讨论问题,可继续补充新需求或进入工程初始化阶段。