# Agent 文档工具设计 > 日期:2026-08-09 > 状态:已确认,待编写实施计划 ## 背景与目标 当前 agent 工具系统已有 14 种工具类型(fetch、calculator、datetime 等),但缺少文档读写能力。agent 无法跨会话持久化重要信息,长对话上下文丢失后无法恢复。 本设计新增 `document` 工具类型,让 agent 能够: 1. **记录**:把重要信息、决策、中间结论写入文档,后续会话可读取(解决上下文丢失) 2. **产出**:生成面向用户交付的文档(如周报、需求文档、会议纪要) 核心诉求是"优化上下文与记录"——通过摘要 + token 预算控制,避免把所有历史记录塞进上下文。 ## 设计决策 | 决策项 | 选择 | 理由 | |--------|------|------| | 核心定位 | 记录 + 产出兼顾 | 既作 agent 内部记忆,也作交付物 | | 存储模型 | 新建独立表 | 与 articles 解耦,不污染文章系统语义 | | 工具粒度 | 单工具 + 内部分发 | agent 侧一个工具,内部按 action 分发到子文件 | | 上下文优化 | 摘要 + token 预算 | 零额外依赖,后续可演进到向量检索 | | 权限范围 | 用户级隔离 | agent 只能读写当前用户的文档 | | 文档类型 | memory/document 二分 | memory=内部记忆,document=交付物 | | 交付物处理 | 独立查看 | 不与 articles 打通,前端独立查看页 | | 审批策略 | config 危险操作白名单 | delete 默认需审批,复用现有审批基础设施 | | 前端范围 | 后端 + 查看页 | 列表 + 详情(markdown 渲染 + 编辑) | ## 第 1 章:数据模型 ### 新建表:`agent_documents` 文件位置:`packages/drizzle-pkg/lib/schema/agent-document.ts` | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | `id` | text | PK | `ad_${timestamp36}_${random}` | | `userId` | integer | NOT NULL | 所属用户(隔离边界) | | `sessionId` | text | nullable | 关联会话(memory 类型用) | | `type` | text enum | NOT NULL | `memory` \| `document` | | `title` | text(255) | NOT NULL | 标题 | | `content` | text | NOT NULL | 正文(markdown) | | `summary` | text | nullable | 摘要(agent 写入时生成,或后端截断前 200 字符) | | `tags` | text(500) | nullable | 逗号分隔标签 | | `createdAt` | timestamp_ms | NOT NULL, defaultNow | 创建时间 | | `updatedAt` | timestamp_ms | NOT NULL, defaultNow, onUpdate | 更新时间 | ### 索引 - `idx_agent_doc_user_type` ON (userId, type) — 按用户+类型查询 - `idx_agent_doc_user_created` ON (userId, createdAt) — 按用户+时间排序 - `idx_agent_doc_session` ON (sessionId) — 按会话查询 ### 设计说明 - 不设 `status` 字段:memory/document 都不需要 draft/published 语义 - 不设 `visibility` 字段:用户级隔离,无需多级可见性 - `summary` 由 agent 在 create/update 时主动传入;若未传,后端取 content 前 200 字符 - `sessionId` 只能由 agent 工具内部设置,API 不暴露该字段 ## 第 2 章:工具 Executor 设计 ### 文件结构 ``` server/service/agent-tool/executors/document/ ├── config.ts # DocumentToolConfig(zod)+ DEFAULT_CONFIG ├── document.ts # 主 executor,内部分发到各操作 ├── operations/ │ ├── create.ts # 创建文档 │ ├── read.ts # 读取/搜索文档(摘要 + token 预算) │ ├── update.ts # 更新文档 │ ├── delete.ts # 删除文档 │ └── list.ts # 列表(轻量,只返回 id/title/summary/type/updatedAt) └── token-budget.ts # token 预算计算工具 ``` ### Input Schema(zod) ```typescript z.object({ action: z.enum(["create", "read", "update", "delete", "list"]), // create / update 共用 title: z.string().max(255).optional(), content: z.string().optional(), type: z.enum(["memory", "document"]).optional(), summary: z.string().optional(), tags: z.string().optional(), sessionId: z.string().optional(), // read 单个 / update / delete id: z.string().optional(), // read / list 搜索 q: z.string().optional(), filterType: z.enum(["memory", "document"]).optional(), page: z.number().int().min(1).optional(), pageSize: z.number().int().min(1).max(50).optional(), // read 单个时是否返回完整内容 fullContent: z.boolean().optional(), }).superRefine((val, ctx) => { // 按 action 校验必填字段: // create: title + content 必填 // read: id 或 q 至少一个 // update: id + 至少一个可更新字段 // delete: id 必填 // list: 无必填 }); ``` ### Config ```typescript interface DocumentToolConfig { maxContentLength: number; // content 最大字符数,默认 50000 defaultSummaryLength: number; // 默认摘要截断长度,默认 200 tokenBudget: number; // read/list 返回的总 token 预算,默认 2000 maxPageSize: number; // list 最大每页条数,默认 20 approvalRequiredActions: string[]; // 需要审批的操作,默认 ["delete"] } const DEFAULT_DOCUMENT_CONFIG: DocumentToolConfig = { maxContentLength: 50000, defaultSummaryLength: 200, tokenBudget: 2000, maxPageSize: 20, approvalRequiredActions: ["delete"], }; ``` ### buildDescription 根据 config 动态生成,告知 agent 可用操作、类型选项、token 预算限制、需审批的操作。 ### execute 内部分发 ```typescript async execute(input, config, ctx): Promise { // 审批检查 if (config.approvalRequiredActions.includes(input.action) && !ctx.approved) { return { success: false, error: `操作 "${input.action}" 需要用户审批`, metadata: { durationMs: 0, needsApproval: true }, }; } // 未登录检查 if (ctx.userId === null || ctx.userId === undefined) { return { success: false, error: "该工具需要登录会话", metadata: { durationMs: 0 }, }; } switch (input.action) { case "create": return await createDoc(input, config, ctx); case "read": return await readDoc(input, config, ctx); case "update": return await updateDoc(input, config, ctx); case "delete": return await deleteDoc(input, config, ctx); case "list": return await listDocs(input, config, ctx); } } ``` 每个操作函数独立文件,接收 `(input, config, ctx)` 返回 `ToolResult`。 ## 第 3 章:各操作行为与 token 预算 ### create - 校验 title(必填,≤255)、content(必填,≤maxContentLength)、type(默认 `memory`) - summary:若 agent 传入则用,否则取 content 前 `defaultSummaryLength` 字符 - sessionId:若 agent 传入则存,否则 NULL - 写入 DB,返回 `{ id, title, type, createdAt }` ### read(单个) - 按 id + userId 查询(隔离) - 默认返回 summary + metadata;若 `fullContent=true` 则返回完整 content - 若 content 超过 tokenBudget 且未指定 fullContent,返回 summary 并提示 "内容过长,已返回摘要,如需完整内容请指定 fullContent=true" ### list(搜索/列表) - 按 userId + filterType + q(title/content LIKE)过滤 - 分页(默认 page=1, pageSize=10, maxPageSize=20) - 返回数组:`[{ id, title, summary, type, tags, updatedAt }]`(不含 content) - **token 预算控制**:累加每条 summary 的字符数,超过 `tokenBudget * 4`(约 1 token ≈ 4 字符)则截断并返回 `hasMore: true` + 下一页提示 ### update - 按 id + userId 查询(隔离,只能改自己的) - 支持更新 title/content/summary/tags - 若 content 更新且未传 summary,自动重新截断 - 返回 `{ id, title, updatedAt }` ### delete - 按 id + userId 查询(隔离) - 硬删除 - 返回 `{ id, deleted: true }` ### 权限与隔离 - 所有操作都强制 `WHERE userId = ctx.userId` - 若 ctx.userId 为 null(未登录),直接返回错误 - 不做 admin 跨用户读写 ## 第 4 章:审批机制 ### 现有机制回顾 - `agent_tools` 表有 `needsApproval` 字段(工具级开关) - `agent_tool_associations` 表有 `needsApproval`(agent 维度覆盖工具级) - 审批在 `chat-engine.ts` 中通过 `approvalToolCallId` 流程实现,粒度是"工具调用" ### 单工具审批方案 在 `DocumentToolConfig` 中增加 `approvalRequiredActions: string[]`,默认 `["delete"]`。 executor 在 execute 时检查:若 action 在白名单中且 `ctx.approved` 不为 true,返回 `{ success: false, error: "操作需要用户审批", metadata: { needsApproval: true } }`,触发现有审批流程。 ### ToolContext 扩展 在 `server/service/agent-tool/registry.ts` 的 `ToolContext` 接口中增加 `approved?: boolean` 字段,由 `executeAgentTool` 在审批流程中注入。 ```typescript export interface ToolContext { toolId: string; toolSlug: string; userId: number | null; approved?: boolean; // 新增:是否已通过审批 } ``` `executeAgentTool` 函数在构造 ctx 时,根据调用来源设置 `approved`: - 正常调用(非审批流程):`approved = false` - 审批通过后继续执行:`approved = true` ## 第 5 章:Service 层与 API ### Service 层 新建 `server/service/agent-document/index.ts`,提供 CRUD 函数(供 executor 和 API 共用): ```typescript createDocument(userId: number, input: CreateDocumentInput): Promise getDocumentById(userId: number, id: string): Promise listDocuments(userId: number, opts: ListDocumentsOptions): Promise<{ items, total, page, pageSize, hasMore }> updateDocument(userId: number, id: string, input: UpdateDocumentInput): Promise deleteDocument(userId: number, id: string): Promise ``` 所有函数内部都强制 `WHERE userId = ?`,executor 和 API 都调用这些函数,确保隔离逻辑单点维护。 ### API 路由 ``` server/api/agent-documents/ ├── index.get.ts # 列表(分页 + type/q 过滤) ├── index.post.ts # 创建(用户手动创建文档,非 agent 调用) ├── [id].get.ts # 详情 ├── [id].put.ts # 更新 └── [id].delete.ts # 删除 ``` - 所有 API 都从会话取 userId,未登录返回 401 - API 不暴露 sessionId 字段(sessionId 只能由 agent 工具内部设置) - API 用于前端查看页的增删改查 ### 前端查看页 - `app/pages/agent-documents/index.vue`:文档列表页,按 type 分 tab(memory/document),支持搜索 - `app/pages/agent-documents/[id].vue`:文档详情页,markdown 渲染(复用现有 bytemd),支持编辑 ## 第 6 章:注册集成 1. **Schema**:在 `packages/drizzle-pkg/lib/schema/agent-document.ts` 新建表,在 `packages/drizzle-pkg/lib/schema/index.ts` 导出 2. **迁移**:运行 `bun run db:generate` + `bun run db:migrate` 3. **Executor 注册**:在 `server/service/agent-tool/index.ts` 的 `TOOL_TYPE_REGISTRY` 中添加 `document` 条目,import 各文件 4. **AgentToolTypes 枚举**:在 `packages/drizzle-pkg/lib/schema/agent-tool.ts` 的 `AgentToolTypes` 数组中添加 `"document"` 5. **ToolContext 扩展**:在 `registry.ts` 中增加 `approved?: boolean` 字段 6. **executeAgentTool 扩展**:在 `index.ts` 的 `executeAgentTool` 中根据调用来源设置 `ctx.approved` 7. **Seed**:可选,在 seed 中创建一个默认的 document 工具实例 ## 第 7 章:测试策略 ### 单元测试 各 operation 函数(create/read/update/delete/list)独立测试,mock db。 ### 集成测试 executor 端到端,用测试数据库。 ### E2E 测试 在现有 `e2e/` 框架下新增 `specs/agent-document.spec.ts`,测试: - agent 对话中调用 document 工具创建 memory - 后续会话中 agent 读取该 memory - 前端查看页可看到 agent 创建的文档 - delete 操作触发审批流程 ### 错误处理 - 输入校验失败:返回 zod 错误信息 - 文档不存在/无权限:返回 `success: false, error: "文档不存在或无权访问"` - 未登录:返回 `success: false, error: "该工具需要登录会话"` - content 超长:返回 `success: false, error: "内容超过最大长度限制"` ## 演进路径 当前方案 A(纯 SQL + 摘要)的后续演进方向: 1. **向量检索**:在 summary 字段基础上加 embedding 字段,引入 embedding 模型 + sqlite-vss,实现语义检索 2. **分层记忆**:memory 细分为 session_memory(绑定 sessionId,会话结束自动归档)和 long_term_memory 3. **文档版本**:增加版本历史,支持回溯 4. **文档分享**:增加 visibility 字段,支持 shared/public 可见性