21 KiB
AI 写作「四阶段内置智能体化」改造方案
目标:把 AI 写作现有的 素材收集专家 / 写作大纲 / 作家写作 / 编辑审核 四个功能, 从「硬编码的 LangGraph 节点 + 硬编码提示词」改造为由内置功能型智能体驱动—— 每个阶段对应一个可配置的内置 agent(独立人设 SOUL、可配模型、可挂技能/工具)。
本文给出现状梳理、目标设计、以及分步可独立交付的实现计划。
0. 一句话结论
不推倒重来。保留现有图的编排骨架、4 个用户干预点、流式与状态机,只把每个阶段节点的「大脑」从「硬编码 prompt 直调 LLM」替换为「加载对应内置 agent 的 SOUL+配置、在子线程上跑该 agent、按结构化契约解析其输出」。 这样前端几乎不动,复杂逻辑(严格模式、素材不足求助、逐章流式、技能检索步骤)全部保留。
1. 现状梳理(精确到文件)
AI 写作是一条 LangGraph 图:packages/harness/deerflow/agents/ai_writing/graph.py
intent_parser → researcher → (pause 素材确认) → writer_outline → (pause 大纲确认)
→ writer_draft → (pause 草稿确认 / 素材不足求助) → editor → (pause 编辑打回) → done
四个待改造阶段(均为硬编码节点,直接调 LLM、不走 agent 运行时):
| 阶段(用户语言) | 节点文件 | 提示词 | 产出(state.py) |
|---|---|---|---|
| 素材收集专家 | nodes/researcher.py |
prompts/researcher_prompts.py |
material_package(keywords + materials) |
| 写作大纲 | nodes/writer_outline.py |
prompts/writer_prompts.py |
current_outline(Outline JSON) |
| 作家写作 | nodes/writer_draft.py |
prompts/writer_prompts.py |
current_draft(Markdown,逐章流式) |
| 编辑审核 | nodes/editor.py |
prompts/editor_prompts.py |
review_result(评分 + 问题列表 JSON) |
关键事实:
- LLM 调用方式:节点通过
nodes/_utils.py的get_model()+llm_json()直接调 LLM,提示词写死在prompts/*.py。没有走 agent 人设 / 技能 / 工具体系。 - 模型:全程单一
state.model_name(用户在写作表单里选的主模型),各阶段无法分别配。 - 干预点 / 流式 / 严格模式 / 素材不足求助:都在
graph.py的 pause 节点和writer_draft里,逻辑较重,需原样保留。 - 前端:
frontend-web/src/open-canvas/contexts/AIWritingContext.tsx是状态机(researching/writing_outline/writing_draft/reviewing/...),时间线AIWritingTimeline.tsx按progress_events(已带agent_name字段)渲染。
2. 可复用的现成机制(不用造轮子)
代码库里已经有一套成熟的「内置功能型智能体」范式——圆桌的 roundtable-intent / roundtable-recommender / roundtable-coordinator:
- 资源即代码:
app/gateway/routers/_roundtable_seed_assets/<agent-id>/config.yaml(id/name/description,可扩展model/skills/tool_groups)SOUL.md(人设 + 职责 + 工作流 + 结构化输出契约)
- 种子器:
app/gateway/routers/_roundtable_seed.py::ensure_roundtable_functional_agents- 启动时 + 相关路由入口自愈:目录不存在才从资源字节级落盘到
.deer-flow/agents/<id>/+ DB(已存在则永不覆盖,尊重本地编辑)。
- 启动时 + 相关路由入口自愈:目录不存在才从资源字节级落盘到
- 运行:在独立 thread 上跑该内置 agent(见
app/gateway/routers/intent.py,INTENT_AGENT_ID = "roundtable-intent"),通过 lead agent 运行时把 SOUL.md 作系统提示,流式经 LangGraphcustom/messages通道透传。 - 结构化输出约定:SOUL 规定模型末尾输出固定 marker +
json(如[INTENT_READY]\n```json{...}```),后端用正则解析(intent.py::_INTENT_READY_PATTERN`)。
我们要做的,本质就是照搬这套范式,给 AI 写作做 4 个功能型 agent,并让对应的图节点改成「跑这个 agent + 解析其结构化输出」。
另有 agents 通用体系可复用:agents 表、/api/agents CRUD、智能体管理页(配模型/技能/工具)——4 个写作 agent 落盘后天然出现在这里,可被 admin 编辑。
3. 目标设计
3.1 四个内置写作智能体(建议 id / 职责)
| Agent id | 名称 | 取代节点 | 结构化输出 marker | 产出 schema |
|---|---|---|---|---|
ai-writing-researcher |
素材收集专家 | researcher |
[MATERIALS_READY] |
MaterialPackage |
ai-writing-outliner |
大纲规划师 | writer_outline |
[OUTLINE_READY] |
Outline |
ai-writing-writer |
作家 | writer_draft |
[SECTION_READY](逐章) |
章节 Markdown |
ai-writing-editor |
编辑 | editor |
[REVIEW_READY] |
ReviewResult |
每个 agent 自带:
config.yaml:id / name / description+(扩展)model(该阶段专用模型,可空=回退用户主模型)、skills(如 researcher 挂检索技能)、tool_groups。SOUL.md:由现有prompts/*.py内容迁移改写,追加结构化输出契约(严格 marker + JSON 格式 + 反例,仿roundtable-intentSOUL 的「硬规则」写法,压制弱模型乱输出)。
3.2 节点改造方式(核心)
每个阶段节点保留输入/输出契约不变(仍读写 state 的同一批字段),内部从:
# 旧:硬编码 prompt 直调
model = get_model(config, state["model_name"])
result = llm_json(model, SYSTEM_PROMPT, USER_PROMPT.format(...))
改为:
# 新:跑对应内置 agent,按 marker 解析
result = await run_writing_agent(
agent_id="ai-writing-outliner",
state=state, config=config,
user_payload=<本阶段输入>, # 意图/素材/大纲/草稿等拼成的一段输入
marker="[OUTLINE_READY]", # 结构化输出契约
)
新增统一助手 nodes/_agent_runner.py::run_writing_agent(...):在子 thread 上跑指定内置 agent(复用 intent.py 的 run/stream 机制)、把流式 chunk 透传到 custom 通道(前端时间线不回归)、用复用的 _utils.parse_json 多级容错解析 marker 后的 JSON。
3.3 为什么选这个方案(A)而非全多智能体重写(B)
- 方案 A(推荐):保留图骨架,逐节点替换大脑。前端基本不动;干预点/流式/严格模式/素材求助原样保留;可按阶段灰度 + feature flag 回退,风险可控。
- 方案 B(不推荐):仿圆桌重做成多 agent 编排,需重写编排、状态、流式、4 个干预点对接,工作量数倍、回归面巨大,收益(更"纯"的多 agent)对本场景并不必要。
4. 配置与可管理性
- 落盘:仿 roundtable 新增
_ai_writing_seed_assets/+ 一个ensure_ai_writing_functional_agents()种子器,在 Gateway 启动与 ai_writing 会话入口自愈。 - 管理界面:4 个 agent 落盘后即出现在智能体管理页,admin 可编辑 SOUL(人设)/ 模型 / 挂载技能。可选:在「文章类型配置页」旁加一个「写作智能体配置」入口,或给这 4 个打一个
tag便于筛选。 - 模型优先级:节点跑 agent 时
该 agent config.model > 用户写作表单选的主模型 model_name(保留现有"用户选模型"语义,新增 per-阶段覆盖能力)。 - 技能:researcher 现有的技能检索能力(
search/skill_search.py)改为ai-writing-researcher的挂载技能,admin 可增减;检索步骤事件(SkillStep)形态保留给前端展示。
5. 分步实现计划(每步可独立交付 / 验收)
Phase 0 · 契约冻结
- 定 4 个 agent 的
id / name、每个的 marker + JSON schema(与state.py的MaterialPackage/Outline/DraftArticle/ReviewResult严格对齐)。 - 验收:本文 §3.1 / §6 的契约评审通过、字段与现有 TypedDict 一一对应。
Phase 1 · 内置 agent 种子化(后端,纯新增,不接管流程)
- 新建
app/gateway/routers/_ai_writing_seed_assets/<4 个 agent>/{config.yaml, SOUL.md};SOUL 由现有prompts/*.py迁移改写 + 追加结构化输出契约。 - 仿
_roundtable_seed.py写ensure_ai_writing_functional_agents(),挂到启动 lifespan + ai_writing 入口。 - 验收:启动后 4 个 agent 出现在
.deer-flow/agents/、DB、/api/agents,能在智能体管理页查看/编辑;此阶段不改图,写作流程行为不变。
Phase 2 · 节点接入「跑内置 agent」(后端,逐阶段灰度)
- 新增
nodes/_agent_runner.py::run_writing_agent(...)(子线程跑 agent + 流式透传 + marker 解析)。 - 加 feature flag:
config.yaml → ai_writing.use_builtin_agents.{editor,outliner,researcher,writer}(默认 false=走旧节点)。 - 接入顺序(由易到难):
- editor(最简单:小 JSON、无检索、无逐章流式)→ flag 打开端到端验证。
- outliner(输出 Outline JSON)。
- researcher(保留技能检索步骤事件、素材确认暂停)。
- writer(最难:保留逐章流式 + 严格模式素材不足求助
blocked_sections;建议仍由节点控制"逐章循环",每章调一次ai-writing-writer,section_help暂停逻辑不动)。
- 每接一个,旧 prompt 与旧逻辑保留,节点按 flag 二选一。
- 验收:每个阶段 flag 打开后端到端走通,产出结构与旧版一致,干预点 / 流式 / 严格模式 / 求助暂停均不回归。
Phase 3 · 每阶段 agent 配置化
- per-agent 模型覆盖生效(
agent.config.model > model_name)。 - researcher 的检索能力改为挂载技能,admin 可增减。
- 智能体管理页可编辑这 4 个的 SOUL / 模型 / 技能;config 热加载,新会话即生效、不重启前端。
- 验收:admin 改某阶段 agent 的模型 / SOUL / 技能后,新会话立即按新配置运行。
Phase 4 · 前端适配(小改)
- 时间线 / 状态文案:阶段名读对应 agent 的
name(progress_events.agent_name已存在,基本沿用,去掉写死文案)。 - 可选:写作表单 / 设置加「查看·配置写作智能体」入口,跳智能体管理页。
- 验收:前端展示各阶段对应的智能体名;配置入口可达。
Phase 5 · 清理收口
- 四阶段稳定后,默认打开全部 flag、移除旧 prompt 直调路径与 flag(prompts 历史留在 git)。
- 更新
offline-backend-20260512/backend/CLAUDE.md架构描述。 - 验收:回归测试通过、冗余删除、文档同步。
6. 结构化输出契约(与现有 state 对齐)
各 agent SOUL 末尾必须输出 marker + ```json``` 块,schema 直接对齐 state.py`:
ai-writing-researcher→[MATERIALS_READY]:{ "keywords": string[], "materials": Material[] }(Material见state.py,含id/title/content/source/relevance_score/source_type/...)。ai-writing-outliner→[OUTLINE_READY]:Outline = { "title": string, "sections": OutlineSection[] }(OutlineSection = { section_title, key_points: KeyPoint[], material_ids })。ai-writing-writer→ 逐章[SECTION_READY]:{ "section_title": string, "markdown": string, "needs_more_material"?: { "reason": string } }(needs_more_material触发严格模式求助blocked_sections)。ai-writing-editor→[REVIEW_READY]:ReviewResult = { "verdict": "pass"|"reject", "fact_score", "logic_score", "language_score", "overall_score", "pass_threshold", "revision_notes": ReviewIssue[] }。
解析复用 nodes/_utils.py::parse_json(多级容错),marker 正则仿 intent.py::_INTENT_READY_PATTERN。
7. 风险与注意
- 弱模型结构化输出不稳:SOUL 写严格格式 + 反例 + 「输出 marker 后立刻闭嘴」硬规则(仿
roundtable-intentSOUL §0);解析层复用parse_json多级降级。 - writer 的逐章流式 + 严格模式求助最复杂:建议循环控制权留在节点(每章调一次 agent),不要让 agent 一次吐全文,否则
blocked_sections/section_help/ 逐章流式都要重做。 - researcher 技能检索步骤事件(
SkillStep)前端有专门卡片:迁移时保留这些progress_events形态。 - 种子器 never-overwrite:迭代 SOUL 后老部署不会自动更新(与圆桌同一问题)。运维更新办法:删
.deer-flow/agents/<id>/重新种子,或后续给种子器加「内置功能 agent 按版本/hash 覆盖」开关。 - 子线程隔离:跑写作 agent 的子 thread 与主 ai_writing thread 状态隔离,注意 checkpointer 与清理(沿用 ai_writing 既有的会话清理)。
- 离线内网:researcher 的检索一律走技能(不联网),与现有「离线部署禁 web_search、走 skill」策略一致。
8. 涉及文件清单(落地时对照)
后端(主要改动)
- 新增
app/gateway/routers/_ai_writing_seed_assets/{ai-writing-researcher,ai-writing-outliner,ai-writing-writer,ai-writing-editor}/{config.yaml,SOUL.md} - 新增
app/gateway/routers/_ai_writing_seed.py(种子器,仿_roundtable_seed.py) - 新增
packages/harness/deerflow/agents/ai_writing/nodes/_agent_runner.py(run_writing_agent) - 改
nodes/{researcher,writer_outline,writer_draft,editor}.py(按 flag 二选一) - 改
config.yaml(新增ai_writing.use_builtin_agents.*开关;可选 per-agent 默认模型) - 保留
prompts/*.py(回退用,Phase 5 删)
前端(小改)
- 改
src/open-canvas/components/ai-writing/AIWritingTimeline.tsx(阶段名读 agent name) - 可选:写作表单/设置加「配置写作智能体」入口(跳
/page/...智能体管理页)
文档
- 本文件
- Phase 5 收口时更新
offline-backend-20260512/backend/CLAUDE.md
9. 实施记录(2026-06-11,Phase 1-4 已落地)
已交付
- Phase 1:
_ai_writing_seed_assets/{ai-writing-researcher,ai-writing-outliner,ai-writing-writer,ai-writing-editor}/{config.yaml,SOUL.md}+_ai_writing_seed.py::ensure_ai_writing_functional_agents(),挂启动 lifespan(_sync_legacy_agents之前)+ai_writing.list_sessions / list_article_types入口自愈。 - Phase 2:
nodes/_agent_runner.py(stream_writing_agent/run_writing_agent/parse_marker_json/resolve_agent_model_name/use_builtin_agent);四节点按config.yaml → ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}二选一(默认全 false,config 按 mtime 热加载,改完即对新会话生效)。agent 目录缺失时节点静默回退旧 prompt 路径,写作流程永不因 agent 被删而中断。 - Phase 3(部分):per-agent 模型覆盖已生效(优先级
agent config.model > 节点显式回退模型(keyword_model/rank_model)> 表单主模型 model_name);SOUL/模型在智能体管理页可编辑、按 mtime 热加载。researcher「检索能力改挂载技能」未做(仍走节点内 SkillPicker 编排)。 - Phase 4:时间线 agent 名由后端
progress_events.agent_name动态下发(flag 开启时读 agent config 的name);TimelineStep.tsx补「大纲规划师」配色(未命中名字回退灰色,admin 改名不报错)。 - 测试:
tests/test_ai_writing_seed.py(资源完整性 / 契约 marker / never-overwrite)+tests/test_ai_writing_agent_runner.py(marker 多级解析 / 模型优先级 / flag / 流式端到端 / SOUL 缺失降级),共 31 例。
与 §6 契约的修订(以实现为准)
为保住「逐章流式打字机」「review_chunk/review_item_done 分维度卡片」「素材确认/求助暂停」等前端形态,编排控制权全部留在节点,agent 只承担单次结构化产出:
| Agent | 实际契约 |
|---|---|
ai-writing-researcher |
两个子任务:[KEYWORDS_READY] + {"keywords": []};[MATERIALS_RANKED] + {"ranked_ids": [], "summary": ""}。素材本体来自节点的检索编排(agent 不联网),MaterialPackage 仍由节点组装 |
ai-writing-outliner |
[OUTLINE_READY] + 完整 Outline JSON(新规划 / 修改模式共用,payload 开头标明任务模式) |
ai-writing-writer |
无 JSON marker:每章直接输出 Markdown 正文(保流式);严格模式素材不足只输出一行 [[SECTION_BLOCKED]] 原因(沿用原哨兵) |
ai-writing-editor |
每个维度一次调用:[REVIEW_READY] + {"score": int, "issues": []};综合分/verdict/pass_threshold 仍由节点按 config 计算(不信任 LLM 自判通过) |
另一处架构修订:不在子 thread 上经 lead agent 运行时跑(§3.2 原设想复用 intent.py 机制,但其 loopback 在 app 层,harness 图节点不可 import;且单轮结构化调用无需 thread/checkpointer/中间件成本)。run_writing_agent 直接「SOUL 作 system prompt + 单条输入」调模型,规避了 §7.5 的子线程隔离/清理风险。researcher 挂技能(Phase 3 剩余项)如需完整工具体系再评估接入运行时。
已知存量测试失败(与本次无关)
test_ai_writing_graph.py(mock 已删除的 intent_parser.llm_json)、test_ai_writing_word_count.py(mock 已删除的 writer_draft.get_model)、test_ai_writing_draft_headings.py(断言三级结构升级前的 ####→### 行为)、test_ai_writing_outline_revise.py(断言旧 prompt 文案),改造前即失败,待另行清理。
10. Phase 3 收尾:检索类型两项化 + 配置技能驱动检索(2026-06-13 已实施)
本节落地了 §9 遗留的「researcher 检索改挂载技能」,并按新需求重新设计了检索类型与 PAUSE-1 交互。
10.1 检索类型(material_source)收敛为两项
- 写作表单「素材来源」改名「检索类型」,只剩
general(通用检索,默认)/notebook(我的空间);旧值knowledge_base/skill由 researcher 节点按general兼容(老会话 resume 不受影响)。 - 前端:
AIWritingForm.tsx(选项/标签)、WritingFormContext.tsx(默认值/类型/applyProposal 旧值映射)、types/ai-writing.ts、message-list.tsx(审批卡标签兼容旧值)。
10.2 通用检索 = 素材收集专家配置的技能编排
ai-writing-researcheragent 的config.yaml → skills是权威配置(智能体管理页可增删/替换);种子默认[knowledge-base-search]。- 内置技能 knowledge-base-search(知识库检索):把原 web/intranet 关键词并发检索包装成技能。researcher 对它特殊处理——不拼 description,直接对 Step1 生成的检索词做原生并发检索(行为与旧 knowledge_base 模式一致)。其它配置技能按
意图 + description拼定向 query 检索。按配置顺序执行、累积达max_materials早停(stop_early)。 - 技能配置为空/全部失效 → 内存兜底回退知识库检索(
researcher._load_configured_skills),检索永不瘫痪。 - 种子:
_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md→skills/public/(never-overwrite);旧部署的 researcherconfig.yaml缺skills键时就地补默认(显式skills: []不动)。_ai_writing_seed.py::ensure_ai_writing_functional_agents()一并处理。
10.3 PAUSE-1 输入框 → 匹配技能检索并追加
- 用户在素材确认卡输入自然语言(如「检索 A 的数据」)→
re_search + user_query:配置技能 >1 个时SkillPicker(candidates=配置技能)让模型按输入匹配排序(流式打字机保留),单技能直接用;用用户原话(非知识库技能附 description)检索。 - 追加语义:保留原素材列表与顺序,新素材按 URL 去重后追加在末尾(ID 续号),跳过 LLM 重排与 max_materials 截断(仅 100 条硬上限),summary 显示「追加检索到 X 条新素材,当前共 Y 条」。原 20 条 + 新 20 条 = 40 条。
- 「技能检索」独立按钮已删除(通用检索本来就走技能编排);后端继续兼容旧
enable_skill_searchaction(按 user_query 追加处理)。无输入的「重新搜索」仍为覆盖式重检。 - 前端:
InterventionCard.tsx(按钮/文案,hasQuery 时按钮文案「检索并追加素材」)、MaterialsToolbar.tsx(提示语)、useAIWritingStream.ts(素材卡回到 researching 一律置 skillSearchPending,让 SkillStepsCard 占位即时挂载)。
10.4 测试
tests/test_ai_writing_material_source.py重写(9 例):缺省/旧值→通用检索、自定义技能 description query、user_query 追加+URL 去重+多技能 picker 匹配(candidates 断言)、无输入重检覆盖、空配置回退。tests/test_ai_writing_seed.py扩展(+5 例):技能资产存在、researcher 种子含默认 skills、技能种子 never-overwrite、缺 skills 键就地补丁、显式[]不动。