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

201 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`