Files
eryao/docs/bugs/2026-04-08-followup-sign-level-regeneration.md
T
qzl e80a82bef4 docs: 更新协议文档,删除废弃计划文档
- 更新 http-error-codes, user-points-chat-data-protocol
- 更新 divination-run-protocol, profile-protocol
- 删除废弃的后端和前端设计计划文档
2026-04-08 17:23:02 +08:00

6.5 KiB
Raw Blame History

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_modechat 还是 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",  # <-- 追问时不应发射此事件
        ...
    )
    
  2. 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(...),  # <-- 追问时不应包含
        },
        ...
    )
    
  3. 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"),  # <-- 追问时不应有
        ...
    }
    
  4. 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 时,系统仍在:

  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 决定是否推导卦象:

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