You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

12 KiB

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)

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

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 内部分发

async execute(input, config, ctx): Promise<ToolResult> {
  // 审批检查
  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 在审批流程中注入。

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 共用):

createDocument(userId: number, input: CreateDocumentInput): Promise<AgentDocumentRow>
getDocumentById(userId: number, id: string): Promise<AgentDocumentRow | null>
listDocuments(userId: number, opts: ListDocumentsOptions): Promise<{ items, total, page, pageSize, hasMore }>
updateDocument(userId: number, id: string, input: UpdateDocumentInput): Promise<AgentDocumentRow | null>
deleteDocument(userId: number, id: string): Promise<boolean>

所有函数内部都强制 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 可见性