实现Agent对话,合格自动提交,不合格补充材料的能力
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
---
|
||||
last_reviewed: 2026-06-13
|
||||
---
|
||||
|
||||
# llm_query_text 缺少 start/end 事件导致前端不显示思考过程
|
||||
|
||||
## 错误现象
|
||||
|
||||
- 前端只显示 "正在分析文件..." 的文字提示(来自 agent 事件的 `agent_state_change`)
|
||||
- LLM 返回的思考过程气泡和正式回答气泡都不显示
|
||||
- 校验流程的气泡正常显示,但提取流程的气泡缺失
|
||||
- 后端日志正常,LLM 请求成功返回
|
||||
|
||||
## 触发条件
|
||||
|
||||
- `llm_query_text()` 或 `_llm_query_multimodal()` 被 Agent 调度调用
|
||||
- `source_dir` 参数已传入(启用 SSE 流式事件)
|
||||
- 函数内部只发了 `chunk` 和 `reasoning` 事件,缺少 `start` 和 `end`
|
||||
|
||||
## 原因
|
||||
|
||||
前端 `chat.js` 的 `handleLLMStream` 状态机:
|
||||
|
||||
```
|
||||
start -> _createLLMStreamBubble() // 创建聊天气泡 DOM
|
||||
reasoning -> _appendLLMStreamReasoning() // 往气泡追加思考内容
|
||||
chunk -> _appendLLMStreamChunk() // 往气泡追加正式回答
|
||||
end -> _closeLLMStreamBubble() // 关闭气泡,切换完成样式
|
||||
```
|
||||
|
||||
`_appendLLMStreamReasoning` 和 `_appendLLMStreamChunk` 的入口守卫:
|
||||
|
||||
```js
|
||||
if (!llmStreamState.reasoningContent) return;
|
||||
if (!llmStreamState.textContent) return;
|
||||
```
|
||||
|
||||
没有 `start` 事件,`llmStreamState` 就一直是初始空值,后续所有 `reasoning` 和 `chunk` 事件都会被静默丢弃。
|
||||
|
||||
**为什么校验流程正常?** 因为 `validate_semantic_completeness()` 在调用 `llm_query_text` 之前,自己手动发了 `start` 和 `end` 事件,绕过了这个问题。
|
||||
|
||||
## 修复方法
|
||||
|
||||
`llm_query_text` 和 `_llm_query_multimodal` 在 `source_dir` 非空时,必须在流式循环前后发送完整的事件序列:
|
||||
|
||||
```python
|
||||
# 循环前
|
||||
if source_dir:
|
||||
_emit_llm_stream(source_dir, "start", label="正在分析文件...")
|
||||
|
||||
try:
|
||||
for resp in llm.stream_chat(...):
|
||||
# ... chunk / reasoning ...
|
||||
# 循环后
|
||||
if source_dir:
|
||||
_emit_llm_stream(source_dir, "end", label="分析完成")
|
||||
except Exception as e:
|
||||
if source_dir:
|
||||
_emit_llm_stream(source_dir, "error", error=str(e))
|
||||
```
|
||||
|
||||
## 防回归要点
|
||||
|
||||
修改 `llm_query_text` 或 `_llm_query_multimodal` 时,检查事件发送是否完整:
|
||||
|
||||
| 阶段 | 事件 | 必需性 |
|
||||
|------|------|--------|
|
||||
| 循环前 | `start` | 必需(前端创建气泡) |
|
||||
| 循环中 | `chunk` | 可选(无内容时不发) |
|
||||
| 循环中 | `reasoning` | 可选(模型不支持时不发) |
|
||||
| 成功 | `end` | 必需(前端切换完成样式) |
|
||||
| 失败 | `error` | 必需 |
|
||||
|
||||
删除 `start` 或 `end` 会导致前端气泡丢失,是高频回归点。
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
last_reviewed: 2026-06-13
|
||||
---
|
||||
|
||||
# 闭包内复用外层变量名导致 UnboundLocalError
|
||||
|
||||
## 错误现象
|
||||
|
||||
- 补充材料提交后,提取函数正常执行完成
|
||||
- 日志停在 `travel_applications.json` 保存处,后续没有任何 Agent 处理日志
|
||||
- 没有报错、没有异常堆栈,看起来像"停止"
|
||||
- `result.json` 里实际记录了 `{"ok": false, "error": "local variable 'agent_session' referenced before assignment"}`
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 在 Flask 路由中用 `threading.Thread` 启动后台任务
|
||||
- 外层作用域已有一个变量(如 `agent_session`)
|
||||
- 闭包 `_run()` 内对同名变量既读又写:`agent_session = run_agent_round(session_dir, agent_session, ...)`
|
||||
|
||||
## 原因
|
||||
|
||||
Python 变量作用域规则:**只要函数体内有任何对某标识符的赋值,该标识符在整个函数内都被视为局部变量**。
|
||||
|
||||
```python
|
||||
agent_session = add_supplement(session_dir, agent_session, filenames) # 外层变量
|
||||
|
||||
def _run() -> None:
|
||||
# ...
|
||||
agent_session = run_agent_round( # 赋值 -> 整个 _run 内 agent_session 是局部变量
|
||||
session_dir, agent_session, # 读局部变量,但此时还未赋值 -> UnboundLocalError
|
||||
new_files=filenames,
|
||||
)
|
||||
```
|
||||
|
||||
`run_agent_round` 调用时,`agent_session` 作为参数被求值,但此时它还是未初始化的局部变量,触发 `UnboundLocalError`。该异常被 `except BaseException` 捕获后写入 result.json,没有在日志中输出,所以表现为"静默停止"。
|
||||
|
||||
## 修复方法
|
||||
|
||||
闭包内使用不同名称接收返回值:
|
||||
|
||||
```python
|
||||
# 修复前
|
||||
agent_session = run_agent_round(session_dir, agent_session, new_files=filenames)
|
||||
|
||||
# 修复后
|
||||
new_session = run_agent_round(session_dir, agent_session, new_files=filenames)
|
||||
```
|
||||
|
||||
后续对返回值的引用统一改为 `new_session`。
|
||||
|
||||
## 防回归要点
|
||||
|
||||
| 场景 | 风险 | 检查方法 |
|
||||
|------|------|----------|
|
||||
| 在闭包/嵌套函数内赋值与外层同名的变量 | UnboundLocalError | ruff F823 规则 |
|
||||
| `except BaseException` 吞掉异常且不打日志 | 静默失败,难以排查 | 至少记录 `log.exception` |
|
||||
| 用 `# noqa: F823` 压制警告而不修复 | 问题持续存在 | noqa 只应用于确认安全的场景 |
|
||||
|
||||
**核心原则**:在闭包内需要接收外层变量的返回值时,始终使用不同的变量名。不要依赖 `nonlocal` 来修复合法性问题——换名字更简单、更安全。
|
||||
Reference in New Issue
Block a user