architecture adr ADR 004 domain aware agent profiles - WangYajun369/ai-writing-platform GitHub Wiki

ADR-004:Agent 领域画像与能力技能正交分层

状态:提议 日期:2026-10-09 影响范围:Agent 引擎 / 数据模型 / 前端 AI 面板 适用版本:1.23.0 | 最后核对:2026-10-15

背景

用户希望「把 AI 助手封装为 Agent,方便后期扩展」,并举例:小说作品用小说 Agent,论文作品用论文 Agent,书籍作品用拆书 Agent,学科笔记作品用学科笔记 Agent(记录学习笔记),且可以自己选择作品使用哪个 Agent。

在动手设计前,对现状做了全量核查,得到三个决定方案走向的事实:

事实一:Agent 引擎已经存在,不需要重建。

commands/agent/ 是一套从 Python LangGraph 迁移来的 Rust 原生实现,含 ReAct 流式循环、6 个数据库工具、三层记忆(memories 表)、推理轨迹(agent_traces 表)。更关键的是——常规「AI 聊天」模式早已走这套引擎(useAiChat 调用的是 agentApi.executeSkill,默认 skill='writing'),而 stream_ai_chat 这条直连链路全项目只剩 AI 工具箱一个调用点。所以这是一套引擎、两种 UI 呈现,不是并行两套。

事实二:现有 Skill 是「能力维度」,而需求描述的是「领域维度」。

现有 4 个 Skill(writing / analysis / research / polish)回答的是「现在要干什么」;需求里的「小说 / 论文 / 拆书 / 学科笔记」回答的是「这是一本什么书」。二者是正交的——同一份 writing 提示词目前对任何作品都一模一样,模型完全不知道自己在写小说、写论文还是整理学习笔记。

事实三:Skill 定义散落 7 处硬编码,是最大的扩展障碍。

新增一个 Skill 需要同时改:prompts.rs 的基准提示与动态提示、tools.rs 的工具子集、engine.rs 的执行预算、前端 agent/types.ts 的类型与元数据、ai/panel/constants.ts 的快捷语(穷举 switch,漏改直接编译报错)。没有统一的数据结构、没有注册表、没有配置段、没有数据库表。

此外两个约束:

  • 作品类型字段完全不存在。Book 模型前后端各 14 个字段,无 book_type / genre / kind 之类字段。
  • 作品大纲进不了模型上下文。工具集只有 6 个(读章节 / 读章节块 / 读章节摘要 / 章节列表 / 检索世界观 / 整书概览),没有任何工具能读到 books.outline 或 chapters.outline,大纲目前只在前端做「是否存在」的前置校验,内容本身模型看不到。

备选方案

维度关系

方案 优点 缺点
A. 领域与能力双层正交 语义正确;book_type 回答「什么书」、Skill 回答「干什么」;组合自由(论文 + 润色可行) UI 需多一个选择器;Prompt 组装要处理两段拼接
B. 领域直接作为新 Skill 值 改动最小,复用现有 chips 语义混乱;chips 从 4 个变 7 个;「论文 + 润色」无法表达;用户会困惑该选哪个
C. 领域 + 能力预组合成 Profile UI 最简洁,用户只选一个 组合爆炸(4 × 4 = 16 种,且领域仍在增长);灵活性差;新增维度要重排全部组合

画像定义存放位置

方案 优点 缺点
A. Rust 注册表(新建 profiles.rs) 类型安全;可写单测;顺带收敛现有 7 处硬编码;工具集与预算可与提示词绑定 改提示词需重新编译
B. app_config 新增 agent_profiles 段 运行时可改,用户可自定义 长文本在配置里编辑体验差;需扩 ConfigSection 枚举与前端 client;宽松校验下坏数据易入库
C. 内置 + 配置覆盖 灵活度最高 两套来源合并逻辑复杂;出问题时难判断生效的是哪一份

作品类型落库方式

方案 优点 缺点
A. Book 新增 book_type 字段 按作品持久记忆;可随备份迁移;是「作品级属性」的正确归属 改动面约 18 处(含 DDL、迁移、备份导出 / 导入 / 一致性校验);需补一个「编辑作品」弹窗(当前只有新建弹窗)
B. 不落库,会话内选择 零数据迁移,可立刻用起来 换设备 / 重装丢失;无法「按书记住」
C. 复用现有 tags 推断 零 DDL 改动 tags 是自由文本,不可靠;用户无感知

决策

采用 A + A + A 组合,即:

  1. 领域与能力双层正交。Book.book_type(小说 / 论文 / 拆书 / 学科笔记,作品级)+ Skill(写作 / 分析 / 研究 / 润色,会话级),二者相乘决定最终行为。

    四个领域的定位与差异化要点:

    领域 回答什么 差异化重点
    小说 这是一部虚构叙事作品 情节推进、人物弧光、场景质感、文风一致性
    论文 这是一篇学术论证文本 论点 / 论据 / 引用规范、章节逻辑、术语严谨
    拆书 这是对某本书的拆解消化 章节拆解、要点提炼、卡片化输出、 actionable 结论
    学科笔记 这是某学科的学习记录 概念定义、公式定理、例题与错因、知识点关联与复习导向
  2. 画像与工具双层收敛到 Rust 注册表。新建 commands/agent/profiles.rs,并分两步、先工具后画像:

    • L0 工具注册表:把 tools.rs 中散落的三处 match(订阅 tools_for_skill、定义 tool_schema、分发 execute_tool)合并为一个 ToolDef { name, description, parameters, execute } 数组。工具引擎本就按名字字符串派发(engine.rs 的 react_loop 中 tools::execute_tool(&conn, &tc.name, ...)),且 build_tools_schema 已从名字列表自动生成 schema,因此这一步是低阻力纯重构。
    • L1 画像注册表:用 AgentProfile 结构体数组统一承载「基准提示 + 动态提示 + 工具子集(引用 L0 的工具名)+ 执行预算」,把现有散落 7 处的硬编码收敛为「一处定义、多处查表」。

    顺序不可颠倒:L1 的画像要引用 L0 的工具名,先做 L0 才能让画像定义是纯数据。

  3. 作品类型落库。新增 books.book_type 字段(TEXT NOT NULL DEFAULT ''),新建作品时选择,并补齐「编辑作品」弹窗以便后期修改。

  4. 明确「领域 × 能力」的合并规则(原方案缺失,实施时必然卡住):

    维度 合并规则 理由
    提示词 领域基准 → 能力基准 → 能力动态提示,纯拼接 领域定义「世界观」,能力定义「动作」,后者更具体应靠后
    工具集 取并集 领域工具(如拆章)与能力工具(如读章节)互补,取交集会导致某一侧工具不可用
    执行预算 取较大值 保守策略,避免领域需要的多轮被能力的小预算截断
    动态提示 各自独立命中,领域提示在前 两者关键词域不同,冲突概率低
  5. book_type 值域与兜底语义(原方案缺失):

    存储值 含义 对应画像
    novel 小说 情节 · 人物 · 文风
    thesis 论文 论点 · 论据 · 引用
    breakdown 拆书 拆解 · 卡片 · 要点
    note 学科笔记 概念 · 公式 · 例题 · 复习
    ''(空) 存量作品或未指定 回退 novel

    不引入「通用」画像——多一个画像就多一份维护成本,且「通用」与「小说」的差异对模型而言不明确。未知值同样回退 novel 并记一条告警日志。

Prompt 组装的唯一入口(engine.rs 的 run_skill_inner)相应扩展为四段拼接:

领域基准提示(来自 AgentProfile)  ← 新增
  + 能力基准提示与动态提示(来自 Skill,既有)
  + 历史记忆段(既有)
  + 历史对话摘要段(既有)
  + 固定尾缀(既有)

理由

  1. 不重建引擎。现有 ReAct 引擎、6 个工具、三层记忆、轨迹回放都是可复用资产;需求本质是「提示词与工具集的差异化」,不是「执行机制的替换」。改造集中在 Prompt 组装层与画像定义层。
  2. 正交分层避免组合爆炸。4 个领域 × 4 个能力 = 16 种组合,用两层各存一份(4 + 4 = 8 份定义)即可表达;预组合方案要维护 16 份。领域后续仍可能增长(如「剧本」「公文」),正交分层的边际成本恒为「加 1 份定义」,而预组合是「乘 4 份」。
  3. 注册表顺带还清技术债。方案 2 不只是「加功能」,更把「加一个 Skill 要改 7 处」降为「改 1 处」、「加一个工具要改 3 处」降为「改 1 处」。后者尤其关键——本 ADR 的 follow-up 与实施计划都要新增多个工具(大纲、拆章、概念卡),若不做 L0 收敛,每加一个工具都要在三处 match 间来回改。
  4. book_type 从 book_id 在 Rust 侧反查,而不是让前端多传一个参数。因为工具层的 get_book_context 已经在 Rust 侧按 book_id 读过 Book,顺手取字段即可,前端链路零改动,也避免前端传参与数据库不一致。
  5. 落库而非会话内选择。作品类型本质上是作品的属性而非会话偏好,应当随作品走、随备份迁移;否则「换台电脑所有书都变回默认 Agent」,与需求中「可以自己选择作品使用哪个 Agent」的持久预期不符。

后果

正面

  • 模型终于知道自己在处理什么类型的作品,小说 / 论文 / 拆书 / 学科笔记的语气、结构、术语可差异化
  • 新增一个领域或能力从「改 7 处」降为「改 1 处」
  • 工具集可按领域裁剪(例如拆书 Agent 需要「拆章 / 生成卡片」类工具,学科笔记 Agent 需要「概念卡 / 复习题」类工具,论文 Agent 不需要小说的人物卡工具)
  • 学科笔记可复用现有数据模型,无需新表:作品 = 学科(或课程),卷 = 模块 / 章节组,章节 = 单条笔记,世界观卡片 = 概念卡。这一映射让学科笔记在阶段三即可落地,不必等新实体设计
  • book_type 落库后可被后续功能复用(导出模板、统计口径、默认大纲模板)

负面 / 代价

  • 数据迁移成本:book_type 涉及约 18 处改动,且备份的导出 / 导入 / 一致性校验三处都需同步列清单,漏一处会导致备份往返丢字段(这类问题在 ADR-004 之前的备份审查中已出现过一次:app_config 未纳入备份)
  • 需要新增「编辑作品」弹窗:当前 components/library/ 下只有 NewBookDialog,没有编辑入口,作品类型建后无法修改
  • Prompt 变长:领域提示叠加能力提示会占用更多上下文窗口,需要控制领域提示长度(建议 ≤ 400 字)
  • UI 复杂度上升:AI 面板头部要多一个领域选择器,与既有模式切换(聊天 / Agent)+ 4 个技能 chips 叠加,需重新规划头部布局避免拥挤
  • 默认值语义:存量作品 book_type 为空字符串,统一回退 novel;不引入「通用」画像(理由见决策 5)
  • ⚠️ 执行模式仍是单一 ReAct:本 ADR 只解决了「提示词与工具集的差异化」,没有解决「执行流程的差异化」。而「拆书」本质是「读章节 → 提炼要点 → 生成卡片」的固定流水线,「学科笔记」的「整理 → 出题」同理,它们靠自由 ReAct 循环 + 换提示词做不出稳定效果。这一缺口由 ADR-005 单独处理,本 ADR 的所有决策在其前提下仍成立

需要 follow-up 的事项

  • 🔜 补齐「作品大纲 / 章节大纲」读取工具 —— 拆书 Agent、论文 Agent、学科笔记 Agent 都强依赖大纲(学科笔记里大纲即「知识结构 / 目录」),当前工具集读不到,是比领域提示更实质的能力缺口。须在 L0 工具注册表落地之后再做,否则仍要改三处 match
  • 🔜 厘清「学科笔记」与既有日记、生词本的边界 —— 日记是每日随笔(diaries 表)、生词本是语言词汇(vocab_* 表)、学科笔记是按学科组织的结构化知识(复用 books / chapters / world_cards)。三者定位不同但用户可能混淆,需在新建入口给出说明
  • 🔜 领域选择是否允许「会话级临时覆盖」(不选作品默认类型,临时切成别的)—— 本 ADR 未定,建议二期按用户反馈决定
  • 🔜 AI 工具箱(单轮生成、无工具无记忆)是否也要感知 book_type —— 本 ADR 未覆盖,其链路走 stream_ai_chat,与 Agent 引擎不同源。ADR-005 的 SingleShot 模式提供了归并路径(实施计划阶段六任务 6),但迁移前需先对齐容错能力——工具箱当前有网络重试而 Agent 链路没有
  • 🔜 若后续领域数量增长(超过 5 个),再评估是否迁移到 app_config 配置化
  • ⚠️ 领域提示与能力提示的拼接顺序需要实测:不同模型对「系统提示前后位置」的敏感度不同,建议用同一组测试用例对比效果

相关文档