# Agent 独立化架构文档 > 分支:`feat/agent-independence` | Commit:`adeb09a` | 日期:2026-08-09 ## 目录 - [1. 设计目标](#1-设计目标) - [2. 数据库 Schema](#2-数据库-schema) - [3. 服务端架构](#3-服务端架构) - [4. API 端点](#4-api-端点) - [5. 前端架构](#5-前端架构) - [6. 多 Session 并行流式输出](#6-多-session-并行流式输出) - [7. 标题生成机制](#7-标题生成机制) - [8. 模型解析优先级](#8-模型解析优先级) - [9. 速率限制](#9-速率限制) - [10. 协作接口预留](#10-协作接口预留) - [11. Seed 数据](#11-seed-数据) - [12. 已知问题与后续工作](#12-已知问题与后续工作) --- ## 1. 设计目标 将原本单一的"agent"(全局配置 + 硬编码 system prompt)改造为**独立化、可配置的多 agent 架构**,为多 agent 协作做准备。 ### 核心变更 | 改造前 | 改造后 | |--------|--------| | 全局 system prompt(`/api/agent/system-prompt`) | per-agent `systemPrompt` 字段(DB) | | 工具集全局共享 | per-agent 工具关联(多对多表) | | 模型由用户选择 | 模型优先级:用户 preferred > agent.defaultModelId > 全局 fallback | | 路由 `/api/agent/...` | 路由 `/api/agents/:agentSlug/...` | | 前端单页 `/` | 前端动态路由 `/chat/:agentSlug` | | 单 session 流式 | 多 session 并行流式(instance 隔离) | ### 兼容性 - **Breaking change**:旧 API 路径 `/api/agent/...` 已删除 - DB 迁移策略:清库重建(dev 阶段) - 旧迁移文件全部删除,仅保留单个 `0000_natural_ken_ellis.sql` --- ## 2. 数据库 Schema 文件:`packages/drizzle-pkg/lib/schema/agent.ts` ### 2.1 `agents` 表(新增) Agent 定义表,每个 agent 是一个独立可配置的 AI 助手。 | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `id` | integer PK | auto | 主键 | | `slug` | text(50) unique | - | URL 标识(如 `default`、`coder`) | | `name` | text(100) | - | 显示名称 | | `description` | text | null | 描述 | | `systemPrompt` | text | `""` | 系统提示词 | | `defaultModelId` | integer | null | 默认模型 ID | | `titleModelId` | integer | null | 标题生成模型 ID(fallback 到 defaultModelId) | | `titleStrategy` | text(20) | `"llm"` | 标题策略:`llm` / `first-line` / `none` | | `enableThinking` | integer | `0` | 是否启用思考模式 | | `enableTools` | integer | `1` | 是否启用工具 | | `maxStepCount` | integer | `6` | 最大工具调用步数 | | `isDefault` | integer | `0` | 是否默认 agent(全局唯一) | | `isCallable` | integer | `0` | 是否可被其他 agent 调用(协作接口) | | `sortOrder` | integer | `0` | 排序 | | `enabled` | integer | `1` | 是否启用 | | `createdAt` / `updatedAt` | timestamp_ms | now | 时间戳 | 索引:`agents_enabled_idx`、`agents_sort_order_idx` ### 2.2 `agent_tool_associations` 表(新增) Agent 与工具的多对多关联表。 | 字段 | 类型 | 说明 | |------|------|------| | `id` | integer PK | 主键 | | `agentId` | integer FK → agents.id (cascade) | Agent ID | | `toolId` | text FK → agentTools.id (cascade) | 工具 ID | | `needsApproval` | integer | 是否需要审批(覆盖工具级配置) | | `sortOrder` | integer | 排序 | | `createdAt` | timestamp_ms | 创建时间 | 唯一索引:`agent_tool_assoc_uniq(agentId, toolId)` ### 2.3 `agent_sessions` 表(改造) 新增 `agentId` 外键字段。 | 新增字段 | 类型 | 说明 | |----------|------|------| | `agentId` | integer FK → agents.id (**set null**) | 所属 agent(agent 删除时 session 保留) | > `enableThinking` / `enableTools` 改为**可空**(null 表示继承 agent 配置) 新增索引:`idx_agent_sessions_agent(agentId)` ### 2.4 其他表(未改动) - `agent_messages`:消息表 - `agent_message_feedback`:消息反馈表 --- ## 3. 服务端架构 ### 3.1 目录结构 ``` server/service/agent/ ├── agent.ts # Agent CRUD + 工具关联查询 ├── chat-engine.ts # 核心聊天引擎(executeChat) ├── session.ts # Session CRUD + 消息持久化 ├── title.ts # 标题生成(llm / first-line / none) ├── rate-limit.ts # 速率限制(per-agentSlug 隔离) ├── abort-manager.ts # 流式中断管理 ├── collaboration.ts # 协作接口 stub(预留) ├── stream-buffer.ts # 流缓冲(SSE 重连支持) ├── stored-part.ts # 消息 parts 存储类型 └── types.ts # 类型定义 server/service/llm/ └── model-resolver.ts # 模型解析(用户/系统/任意) ``` ### 3.2 Agent Service (`agent.ts`) 核心函数: | 函数 | 说明 | |------|------| | `getAgentBySlug(slug)` | 按 slug 查询启用的 agent | | `getAgentBySlugAny(slug)` | 按 slug 查询(含禁用) | | `getDefaultAgent()` | 获取默认 agent(isDefault=1,fallback 到 sortOrder 最小) | | `listAgents(onlyEnabled)` | 列出 agent | | `getAgentWithTools(slug)` | Agent + 关联工具 | | `getAgentTools(agentId)` | 获取 agent 的工具列表 | | `createAgent(input)` | 创建 agent(含工具关联 + isDefault 唯一性维护) | | `updateAgent(id, input)` | 更新 agent(含工具关联重建) | | `deleteAgent(id)` | 删除 agent(cascade 删关联,session 的 agentId set null) | ### 3.3 Chat Engine (`chat-engine.ts`) 从原 HTTP handler 中抽离的核心逻辑,函数签名: ```typescript export interface ChatEngineParams { agent: AgentRow; session: AgentSessionRow; user: { id: number; role: string | null } | null; tempToken: string | null; body: { content, editMessageId?, regenerate?, ... }; ip: string; preferredModelId?: number | null; requestId?: string; registerAbort: (sessionId, controller) => void; unregisterAbort: (sessionId) => void; setRateLimitHeaders?: (headers) => void; onClientClose?: (cb) => void; } export async function executeChat(params: ChatEngineParams): Promise ``` 关键流程: 1. 速率限制检查(未登录用户) 2. 模型解析(优先级见 [§8](#8-模型解析优先级)) 3. 构建 ModelMessage(含历史消息 + 工具调用 parts) 4. `streamText()` 流式生成 5. `onFinish` 回调:保存消息 + 触发标题生成(fire-and-forget) 6. `onAbort` 回调:保存已生成内容 ### 3.4 Model Resolver (`model-resolver.ts`) | 函数 | 说明 | |------|------| | `resolveModelForUser(modelId, userId)` | 为用户解析模型(查用户私有 provider) | | `resolveModelAny(modelId)` | 任意解析(系统级,用于未登录用户) | | `toLanguageModel(resolved)` | 转换为 AI SDK 的 `LanguageModel` | | `resolveLanguageModel(provider, modelId)` | 底层:根据 parseMode 创建 OpenAI / OpenAI-Compatible | ### 3.5 Rate Limit (`rate-limit.ts`) - **Key 隔离**:`${agentSlug}:${sessionId}` / `${agentSlug}:${ip}` - **限制**:每 session 20 次、每 IP 每日 50 次 - 仅对未登录用户生效 ### 3.6 Abort Manager (`abort-manager.ts`) 全局 `Map`,支持按 session 中断流式生成。 --- ## 4. API 端点 ### 4.1 Agent 管理 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/agents` | 列出所有 agent | | POST | `/api/agents` | 创建 agent | | GET | `/api/agents/:agentSlug` | 获取 agent 详情 | | PUT | `/api/agents/:agentSlug` | 更新 agent | | DELETE | `/api/agents/:agentSlug` | 删除 agent | | GET | `/api/agents/:agentSlug/info` | 获取 agent 信息(含工具列表,前端初始化用) | ### 4.2 Session 管理 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/agents/:agentSlug/sessions` | 列出 session | | POST | `/api/agents/:agentSlug/sessions` | 创建 session | | GET | `/api/agents/:agentSlug/sessions/:id` | 获取 session 详情 | | PUT | `/api/agents/:agentSlug/sessions/:id` | 更新 session(标题等) | | DELETE | `/api/agents/:agentSlug/sessions/:id` | 删除 session | | PUT | `/api/agents/:agentSlug/sessions/:id/config` | 更新 session 配置(modelId、enableThinking、enableTools) | | GET | `/api/agents/:agentSlug/sessions/:id/messages` | 获取 session 消息 | ### 4.3 聊天 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/agents/:agentSlug/chat` | 发送消息(启动流式) | | GET | `/api/agents/:agentSlug/chat/stream` | SSE 流(断线重连) | | POST | `/api/agents/:agentSlug/chat/stop` | 停止生成 | | POST | `/api/agents/:agentSlug/chat/tool-approve` | 工具审批 | | POST | `/api/agents/:agentSlug/chat/cleanup-approvals` | 清理审批 | ### 4.4 其他 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/agents/:agentSlug/models` | 可用模型列表 | | POST | `/api/agents/:agentSlug/feedback` | 消息反馈 | | POST | `/api/agents/:agentSlug/migrate` | 旧数据迁移 | ### 4.5 已删除的端点 - `GET /api/agent/system-prompt` — 已废弃(system prompt 在 agent 配置中) - 所有 `/api/agent/...` 路径 ### 4.6 鉴权白名单 文件:`packages/common/config/index.ts` `API_ALLOWLIST` 和 `FRONTEND_PAGE_ALLOWLIST` 已更新,支持 `:agentSlug` 参数匹配。 `FRONTEND_PAGE_ALLOWLIST` 新增 `/chat/:agentSlug`,允许未登录用户访问聊天页面。 --- ## 5. 前端架构 ### 5.1 路由 | 路径 | 说明 | |------|------| | `/` | 重定向到 `/chat/default` | | `/chat/:agentSlug` | Agent 聊天页面 | 文件: - `app/pages/index.vue` — 重定向页 - `app/pages/chat/[agentSlug]/index.vue` — 主聊天页面 ### 5.2 Composables #### `useAgentSessions(agentSlug: string)` Session 列表管理,**组件级实例**(非全局)。 关键函数: - `fetchSessions()` — 加载 session 列表 - `createSession()` — 创建新 session - `newSession()` — 智能创建:若最新 session 为空(`lastActiveAt === createdAt`)则复用,否则创建新的 - `selectSession(id)` — 切换 session - `deleteSession(id)` — 删除 session - `touchSessionOrder(id)` — 将 session 移到列表顶部(更新 `lastActiveAt`) - `refreshSessionTitle(id)` — 从服务端同步标题 #### `useAgentChatStore(options)` 多 session 聊天实例管理,**核心架构**。 ``` useAgentChatStore ├── instances: Map # 每个 session 独立实例 ├── currentSessionId: ref ├── currentInstance: computed → 当前 session 的 instance └── messages/isLoading/... : computed → 代理到 currentInstance ``` 关键设计: - **每个 session 有独立的 `AgentChatInstance`**(独立的 `messages` ref、`isLoading` ref、`abortController`) - 切换 session 只是改 `currentSessionId`,旧 instance 的流**继续在后台运行** - `getInstance(sid)` — 获取/创建 instance - `removeInstance(sid)` — 停止 + 清除 + 移除 instance - `stopAll()` — 停止所有 instance 的流(**新建对话时不调用**) - `getLoadingSessionIds()` — 获取所有正在流式输出的 session #### `useAgentChat(options)` 单 session 聊天逻辑,**闭包隔离**。 关键函数: - `send(content, opts?)` — 发送消息 + 流式处理 - `processStream(res, assistantIdx)` — SSE 流解析(操作闭包内的 `messages` ref) - `stopGeneration()` — 停止生成(abort) - `stopAndSave()` — 停止 + 保存已生成内容 - `loadMessages(sid)` — 加载历史消息 - `respondToApproval(toolCallId, approved)` — 工具审批响应 ### 5.3 页面组件 (`app/pages/chat/[agentSlug]/index.vue`) 关键逻辑: ```typescript // 流完成回调 → 3秒后刷新标题 onStreamComplete: (sid) => { setTimeout(() => sessions.refreshSessionTitle(sid), 3000); } // 发送消息 async function handleSend(content: string) { // 1. 确保有 session // 2. touchSessionOrder(sid) — 先更新 lastActiveAt(避免 newSession 复用) // 3. await chat.send(content) // 4. 若是新 session,refreshTitleWithRetry(sid) } // 新建对话 async function handleNewSession() { // 若当前 session 为空且非 loading → 不创建(复用) // 否则 newSession() // 不调用 stopAll() — 其他 session 的流继续运行 } ``` ### 5.4 Auth Routes 文件:`app/utils/auth-routes.ts` `matchesExactOrPrefix` 增强:支持 `:param` 正则匹配(如 `/chat/:agentSlug`)。 --- ## 6. 多 Session 并行流式输出 ### 设计 不同 session 可以**同时**流式输出,新建对话**不会**停止其他 session 的流。 ### 实现原理 1. **Instance 隔离**:每个 session 在 `useAgentChatStore` 中有独立的 `AgentChatInstance`,包含独立的 `messages` ref 和 `abortController` 2. **闭包捕获**:`processStream` 操作的是 instance 闭包内的 `messages`,不是 store 的 computed 3. **切换不中断**:`handleNewSession` 不调用 `stopAll()`,旧 session 的 `processStream` promise 继续在后台执行 4. **数据不串**:切换 session 后,`currentInstance` 指向新 instance,旧 instance 的流写入自己的 `messages` ref ### 验证场景 - 流式输出中点"新对话" → 新 session 创建 → 页面切换到空状态 → 旧 session 流继续 - 旧 session 流完成后 → `onStreamComplete(sid)` 触发 → `refreshSessionTitle(sid)` 更新左侧标题 --- ## 7. 标题生成机制 ### 触发链路 ``` 用户发送首条消息 → chat-engine.ts onFinish 回调 → generateSessionTitle() (fire-and-forget, .catch()) → 根据 agent.titleStrategy: - "none": 跳过 - "first-line": 截取首行(≤50字) - "llm": 调用 titleModelId 生成 → updateSession(sessionId, { title }) ``` ### 前端同步 ``` onStreamComplete(sid) → setTimeout(3s) → refreshSessionTitle(sid) → GET session → 更新 sessions.value 中的 title ``` 新 session 额外重试(`refreshTitleWithRetry`):3s / 6s / 10s / 15s / 22s 多次刷新。 ### 切换 session 的影响 - **服务端**:`onFinish` 在服务端触发,与前端无关,标题会正确写入 DB - **前端**:`onStreamComplete` 在 instance 闭包中,切走 session 后仍会执行;`sessions` ref 是同一组件实例的 ref,`refreshSessionTitle` 仍能找到并更新 session **结论:切换 session 不影响标题生成。** --- ## 8. 模型解析优先级 ``` 已登录用户: session.modelId ?? preferredModelId ?? agent.defaultModelId → resolveModelForUser(modelId, userId) 未登录用户: session.modelId ?? agent.defaultModelId → resolveModelAny(modelId) ``` - `preferredModelId`:用户在前端选择的模型(请求 body 传入) - `session.modelId`:session 级别持久化的模型选择 - `agent.defaultModelId`:agent 配置的默认模型 --- ## 9. 速率限制 | 维度 | Key 格式 | 限制 | |------|----------|------| | Session | `${agentSlug}:${sessionId}` | 20 次 | | IP | `${agentSlug}:${ip}` | 50 次/日 | - 仅对**未登录用户**生效 - 按 agentSlug 隔离(不同 agent 独立计数) --- ## 10. 协作接口预留 文件:`server/service/agent/collaboration.ts` ```typescript export interface AgentInvocation { agentSlug: string; input: string; context?: { sessionId?: string; userId?: number | null }; } export interface AgentInvocationResult { agentSlug: string; output: string; ok: boolean; error?: string; } export async function invokeAgent(invocation: AgentInvocation): Promise ``` 当前为 **stub**,仅校验 agent 存在 + `isCallable` 字段,返回 "not yet implemented"。 `agents.isCallable` 字段控制 agent 是否可被其他 agent 调用。 --- ## 11. Seed 数据 文件:`packages/drizzle-pkg/seed.ts` ### 管理员账号 - 用户名:`admin` - 密码:`123456qaz` ### 工具(11 个) | ID | Slug | 类型 | |----|------|------| | tool_fetch_html | fetch_html | fetch | | tool_fetch_md | fetch_md | fetch | | tool_fetch_json | fetch_json | fetch | | tool_calculator | calculator | calculator | | tool_datetime | datetime | datetime | | tool_uuid | uuid | uuid | | tool_base64 | base64 | base64 | | tool_json_formatter | json_formatter | json-formatter | | tool_regex_tester | regex_tester | regex-tester | | tool_user_info | user_info | user-info | | tool_search | search | search | ### Agent(2 个) | Slug | Name | 工具数 | 特性 | |------|------|--------|------| | `default` | 默认助手 | 11(全部) | enableThinking=0, maxStepCount=6, isDefault=1, titleStrategy=llm | | `coder` | 编程助手 | 3(fetch_md, search, regex_tester) | enableThinking=1, maxStepCount=12, isCallable=1, titleStrategy=first-line | --- ## 12. 已知问题与后续工作 ### 已完成 - [x] DB schema 改造 + seed + 清库重建 - [x] Service 层全部改造 - [x] API 端点创建 + 旧目录删除 - [x] 前端路由迁移 + composables 改造 - [x] default + coder agent 端到端验证 - [x] "输出时新建对话" bug 修复 - [x] auth-routes `:param` 匹配修复 - [x] fetch 工具 config 解析修复(zod safeParse + fallback) ### 待验证 - [ ] coder agent 端到端测试(工具调用、思考模式) - [ ] 跨 agent session 访问拒绝(已验证基本逻辑) - [ ] 多 session 并行流式输出压力测试 ### 后续工作 - [ ] Agent 管理后台 UI(CRUD 界面) - [ ] 协作接口实现(`invokeAgent` 编排逻辑) - [ ] Agent 级别模型配置 UI - [ ] 更多 agent 类型(如 researcher、translator)