Files
eryao/docs/bugs/2026-04-08-followup-sign-level-regeneration.md
T

201 lines
6.5 KiB
Markdown
Raw Normal View 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_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`