Browse Source

feat: 新增 Agent 独立化架构文档,包含设计目标、数据库 Schema、服务端架构及 API 端点信息

feat/agent-independence
npmrun 2 months ago
parent
commit
915c0ba5d0
  1. 517
      docs/agent-architecture.md

517
docs/agent-architecture.md

@ -0,0 +1,517 @@
# 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<ChatEngineResult>
```
关键流程:
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<sessionId, AbortController>`,支持按 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<agentSlug:sid, AgentChatInstance> # 每个 session 独立实例
├── currentSessionId: ref<string | null>
├── 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<AgentInvocationResult>
```
当前为 **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)
Loading…
Cancel
Save