# Bug: 追问时 agent_output 被重新生成,导致 sign_level 被覆盖 日期:2026-04-08 状态:已确认根因(未修复) ## 问题描述 首次解卦完成后,用户继续追问时,agent 会重新生成 `agent_output`,重新计算卦的结论和标签。 **具体表现**: - 首次解卦结论:`中下签` - 追问后结论:`下下签`(同一个卦,结果被重新生成) ## 根因分析 **问题定位**:`backend/src/core/agentscope/runtime/runner.py` ### 根因链条 1. **`runner.py:execute()` 方法(第 70-133 行)** 无论 `runtime_mode` 是 `chat` 还是 `follow_up`,都会执行以下逻辑: ```python # 第 105-119 行:始终推导卦象 derived_divination = self._resolve_derived_divination(run_input=run_input) await self._emit_step_event( pipeline=pipeline, run_input=run_input, step_name="divination", event_type="DIVINATION_DERIVED", # <-- 追问时不应发射此事件 ... ) ``` 2. **`runner.py:_execute_worker_step()` 方法(第 200-245 行)** 始终将 `derived_divination` 传递给 worker: ```python # 第 294-302 行:始终将 divination_derived 放入 worker_output await emitter.emit_final_text_end( worker_output={ **worker_payload.model_dump(mode="json", exclude_none=True), "divination_derived": derived_divination.model_dump(...), # <-- 追问时不应包含 }, ... ) ``` 3. **`stage_emitter.py:emit_final_text_end()` 方法(第 46-73 行)** 始终将所有字段放入 `TEXT_MESSAGE_END` 事件: ```python payload = { "messageId": message_id, "role": "assistant", "stage": self._stage, "status": worker_output.get("status"), "sign_level": worker_output.get("sign_level"), # <-- 追问时不应有 "conclusion": worker_output.get("conclusion", []), # <-- 追问时不应有 "focus_points": worker_output.get("focus_points", []), "advice": worker_output.get("advice", []), "keywords": worker_output.get("keywords", []), "answer": worker_output.get("answer", ""), "error": worker_output.get("error"), "divination_derived": worker_output.get("divination_derived"), # <-- 追问时不应有 ... } ``` 4. **`store.py:_persist_text_message()` 方法(第 125-160 行)** 从事件中提取所有字段并完整存储: ```python worker_output_fields = ( "status", "sign_level", # <-- 追问时被重新生成并覆盖 "conclusion", # <-- 追问时被重新生成并覆盖 "focus_points", "advice", "keywords", "answer", "error", "divination_derived", # <-- 追问时被重新生成 "ui_hints", ) ``` ### 问题本质 `runtime_mode=follow_up` 时,系统仍在: 1. 重新推导卦象(调用 `_resolve_derived_divination`) 2. 发射完整的 `DIVINATION_DERIVED` 事件 3. 生成包含所有结构化字段的 `worker_output` 4. 将所有字段存储到数据库 但根据工程计划(`docs/plans/2026-04-08-followup-session-history-eng-plan.md:37-40`),追问时的预期行为是: ``` [一次追问] user -> /agent/runs(runtime_mode=follow_up) -> assistant(content [+ optional metadata.agent_output.answer]) ``` 即:追问时只输出 `answer`(内容),不重新生成卦象结构。 ## 证据 ### 数据库证据 Session `015fe0f9-0500-43ab-911a-4ce8e3160032`: | 时间 | 角色 | sign_level | divination_derived | 问题 | |------|------|------------|-------------------|------| | 05:21:19 | user | - | - | 首问问题 | | 05:21:28 | assistant | 中下签 | 完整 | 首答(正确) | | 05:22:24 | user | - | - | 追问 | | 05:22:33 | assistant | **下下签** | 完整 | 追问答(sign_level 被重新生成) | 两个 assistant 消息的 `divination_derived` 完全相同,但 `sign_level` 不同,证明是重新生成的。 ## 修复方向 ### 1. `runner.py` 在 `execute()` 方法中,根据 `runtime_mode` 决定是否推导卦象: ```python async def execute(self, ...): runtime_mode = self._resolve_runtime_mode(run_input=run_input) if runtime_mode == RuntimeMode.CHAT: derived_divination = self._resolve_derived_divination(run_input=run_input) await self._emit_step_event(...) # DIVINATION_DERIVED else: derived_divination = None # follow_up 不推导 ``` ### 2. `stage_emitter.py` `emit_final_text_end()` 根据 `runtime_mode` 决定发送哪些字段: ```python async def emit_final_text_end(self, ..., runtime_mode: str): payload = {"messageId": ..., "role": "assistant", "stage": self._stage} if runtime_mode == "chat": payload.update({ "status": worker_output.get("status"), "sign_level": worker_output.get("sign_level"), "conclusion": worker_output.get("conclusion", []), "focus_points": worker_output.get("focus_points", []), "advice": worker_output.get("advice", []), "keywords": worker_output.get("keywords", []), "divination_derived": worker_output.get("divination_derived"), ... }) else: # follow_up payload["answer"] = worker_output.get("answer", "") ``` ### 3. `store.py` `_persist_text_message()` 根据 `runtime_mode` 决定提取哪些字段: ```python async def _persist_text_message(self, ...): runtime_mode = self._resolve_runtime_mode(event=event) if runtime_mode == "chat": worker_output_fields = ("status", "sign_level", "conclusion", ...) else: # follow_up worker_output_fields = ("answer",) # 只存储 answer ``` ### 4. `runtime_models.py` `resolve_worker_output_model()` 应根据 `runtime_mode` 返回不同 schema: ```python def resolve_worker_output_model(runtime_mode: RuntimeMode = RuntimeMode.CHAT) -> type[WorkerAgentOutputLite]: if runtime_mode == RuntimeMode.FOLLOW_UP: return WorkerAgentOutputLite # 只有 answer return AgentOutput # 完整结构(继承自 WorkerAgentOutputRich) ``` ## 相关文件 - `backend/src/core/agentscope/runtime/runner.py` - 主编排逻辑 - `backend/src/core/agentscope/runtime/stage_emitter.py` - 事件发射 - `backend/src/core/agentscope/events/store.py` - 事件持久化 - `backend/src/schemas/agent/runtime_models.py` - 输出 schema 定义 ## 相关文档 - 工程计划:`docs/plans/2026-04-08-followup-session-history-eng-plan.md` - 协议文档:`docs/protocols/divination/divination-run-protocol.md`