- 更新 http-error-codes, user-points-chat-data-protocol - 更新 divination-run-protocol, profile-protocol - 删除废弃的后端和前端设计计划文档
6.5 KiB
Bug: 追问时 agent_output 被重新生成,导致 sign_level 被覆盖
日期:2026-04-08 状态:已确认根因(未修复)
问题描述
首次解卦完成后,用户继续追问时,agent 会重新生成 agent_output,重新计算卦的结论和标签。
具体表现:
- 首次解卦结论:
中下签 - 追问后结论:
下下签(同一个卦,结果被重新生成)
根因分析
问题定位:backend/src/core/agentscope/runtime/runner.py
根因链条
-
runner.py:execute()方法(第 70-133 行)无论
runtime_mode是chat还是follow_up,都会执行以下逻辑:# 第 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", # <-- 追问时不应发射此事件 ... ) -
runner.py:_execute_worker_step()方法(第 200-245 行)始终将
derived_divination传递给 worker:# 第 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(...), # <-- 追问时不应包含 }, ... ) -
stage_emitter.py:emit_final_text_end()方法(第 46-73 行)始终将所有字段放入
TEXT_MESSAGE_END事件: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"), # <-- 追问时不应有 ... } -
store.py:_persist_text_message()方法(第 125-160 行)从事件中提取所有字段并完整存储:
worker_output_fields = ( "status", "sign_level", # <-- 追问时被重新生成并覆盖 "conclusion", # <-- 追问时被重新生成并覆盖 "focus_points", "advice", "keywords", "answer", "error", "divination_derived", # <-- 追问时被重新生成 "ui_hints", )
问题本质
runtime_mode=follow_up 时,系统仍在:
- 重新推导卦象(调用
_resolve_derived_divination) - 发射完整的
DIVINATION_DERIVED事件 - 生成包含所有结构化字段的
worker_output - 将所有字段存储到数据库
但根据工程计划(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 决定是否推导卦象:
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 决定发送哪些字段:
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 决定提取哪些字段:
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:
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