--- name: Story route append context overview: 候选载荷增胖(summary 优先 + 可配置预算)+ category-aware prompt 纠偏 + 最小测试集;不扩大路由体系、不改 schema、不接离线合并。 todos: - id: config-budget content: Add Settings — story_route_candidate_body_max_chars, total_max, head/tail, summary_min_len (optional) status: completed - id: payload-builder content: build_route_candidate_rows — fixed sort, summary-first body rules, total budget downgrade to index rows status: completed - id: prompts-merge-bias content: get_story_route_prompt + get_story_batch_plan — two-layer criteria, category blocks, remove default-to-new_story status: completed - id: wire-agent content: StoryRouteAgent decide/plan_batch use new builder + prompts; validate_story_batch_plan unchanged; no pipeline signature change status: completed - id: tests content: Builder tests + prompt contains + beliefs append smoke + career new_story smoke + test_story_route_oral_invariant status: completed isProject: false --- # Story Route:候选上下文增胖 + category-aware append 纠偏 ## Ticket / PR 一句话 用更富的候选 JSON(**summary 优先**、再按需补正文)与 **按类目纠偏** 的提示词,修复 `StoryRouteAgent` 在强主题类目下过度 `new_story`;预算 **Settings 化**,默认值 **保守**;**不改** memoir 流水线签名与 DB schema。 ## 根因(代码事实) - `[api/app/agents/memoir/story_route_agent.py](api/app/agents/memoir/story_route_agent.py)`:`preview_chars=220`,同类目下多条短感悟几乎不可区分。 - `[api/app/agents/memoir/prompts.py](api/app/agents/memoir/prompts.py)`:**「若无法自信匹配某一候选,选 new_story」** → 与「主题容器逐步加厚」产品预期相反。 - `Story.summary` 列存在,**路由未用**;仅截 `canonical_markdown`。 ## 本轮 scope(做) | 交付物 | 说明 | | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `[story_route_agent.py](api/app/agents/memoir/story_route_agent.py)` | 新增 candidate **payload builder**,`_build_candidate_json` 改为走 builder | | `[prompts.py](api/app/agents/memoir/prompts.py)` | `get_story_route_prompt`、`get_story_batch_plan_prompt` 纠偏 + **两层决策标准** + **三类 category 规则** | | `[config.py](api/app/core/config.py)` | **3–4 个**预算相关字段(见下) | | 测试 | builder 单测、prompt 片段断言、**2 条类目行为样例**、`[test_story_route_oral_invariant.py](api/tests/test_story_route_oral_invariant.py)` 回归 | ## 本轮 scope(不做) - 离线 / admin 合并历史垃圾 story - schema 变更、二次 merge worker、family 细分子策略扩写、时间跨度阈值 - 扩大路由体系(多模型级联、新端口签名等) ## 1. 候选行结构(summary 主角) 每条候选字典 **必带**:`id`、`title`、`char_count`、`version_count`、`updated_at`(ISO)、`linked_chapters`(保持现有拼接逻辑)。 **内容优先级,严格顺序:** 1. 若 `Story.summary` **非空且达到 `summary` 最小长度阈值**(可配置,如 ≥30 字或复用现有惯例),则带 `summary`,**本轮不带** `body_for_route`(避免长正文冲淡摘要)。 2. 若 summary **缺失或过短**,再构造 `body_for_route`:**短正文**尽量全文;超长用 **head + tail**(中间省略说明),长度受 `story_route_long_body_head_chars` / `tail` 与单篇 cap 约束。 3. 超 **总预算** 时,将该条 **降级为索引行**(仅 `id`、`title`、`char_count`、极短 `preview`,提示中说明索引项优先匹配带 `summary`/`body` 的条目)。 **初版默认值(保守,可线上调大):** - `story_route_candidate_body_max_chars`:**1200–2000**(落地取单值如 **1600**) - `story_route_candidate_total_max_chars`:**12000–18000**(如 **16000**) - head/tail:按现有「长文才切」思路配一对合理默认(如各 **600–800**) ## 2. 排序规则(写死,tie-break) 在进入 builder **之前**对 `candidate_stories` 排序(同 stage 列表): `has_summary(desc) → updated_at(desc) → version_count(desc) → char_count(desc) → id(asc)` 其中 `has_summary`:summary strip 后长度 ≥ 配置的 `summary` 最小长度。 ## 3. 提示词 ### 3.1 去掉保守偏置 删除「拿不准就 `new_story`」;改为:**先看是否与某候选在主题/事件层级上可合并,再决定**。 ### 3.2 两层决策标准(显式写在 prompt 里) - **主题连续性信号**:价值观、关系模式、长期总结、同一反思维度。 - **事件切换信号**:新人物组合、新地点、新时间段、新事件因果链。 指引:`beliefs` / `summary` **更看主题连续性**;`career_*` / `childhood` / `education` **更看事件链**。 ### 3.3 类目规则(第一版只三件) - `**beliefs`、`summary`**:**强容器** → 多条短感悟、同一句式起笔、同价值维度 → **强烈倾向 `append_story`**(指向最匹配的一条候选 id)。 - `**career_*`、`childhood`、`education**`:**强 episode** → 明确不同事件链可 `new_story`;同一经历续问可 append。 - `**family`**:**中性** → 一句话:原则/关系反思倾向 append;**明确的新事件链**可 new;**不**展开长列表例外。 `get_story_batch_plan_prompt` 与 `get_story_route_prompt` **对齐**上述规则。 ## 4. 接线 - `[story_pipeline_sync.py](api/app/features/memoir/story_pipeline_sync.py)` **不改** `StoryRouteAgent` 调用签名。 - `validate_story_batch_plan`、Pydantic 模型 **不变**。 ## 5. 测试(最小集 + 2 条行为) | 测试 | 目的 | | -------- | ----------------------------------------------------------------------------------------------------------------------------- | | Builder | summary 优先不带 body;summary 短则带 body;总预算降级;**排序**稳定 | | Prompt | 类目块、两层标准、`beliefs`/`family` 中性句等 **包含断言** | | **行为 A** | mock LLM:`beliefs` + 两则短感悟口述 + 已有 1 条 story → 期望 **append**(或断言传给 mock 的 payload 含足够 summary/body 且 prompt 强调强容器;实现时二选一并写死断言) | | **行为 B** | mock LLM:`career_achievement`(或 `childhood`)+ 两起明确不同事件 → 允许/期望 **new_story** | | 回归 | `[test_story_route_oral_invariant.py](api/tests/test_story_route_oral_invariant.py)`:路由输入仍 **不含 evidence** | 行为测试若不便绑定真实 LLM,采用 **mock `invoke_json_object` / StoryRouteAgent** 固定返回或 **断言 prompt + candidate JSON 形状**,与现有 `[test_story_route_oral_invariant.py](api/tests/test_story_route_oral_invariant.py)` 风格一致。 ## 6. 风险与验收 - **Token**:默认保守;观察 staging 日志 `route_decision` / `is_append` 再调 `Settings`。 - **过度合并**:靠 episode 类与「事件切换信号」段落缓解。 ## 实施顺序 1. `config.py` 字段 2. Builder + builder 单测 + 排序单测 3. Prompt 改造 + prompt 断言 4. `StoryRouteAgent` 接线 5. 行为 A/B + oral invariant + 相关 memoir 测试