Implement follow-up detection in dialog context to prevent context pollution in multi-topic conversations. Introduce is_likely_follow_up function to determine when to inject session history based on user input. Adjust _load_session_text2sql_context to conditionally include context based on follow-up status, enhancing SQL generation accuracy. Update relevant API endpoints to pass user text for improved context handling.

This commit is contained in:
陈辅元
2026-04-15 17:33:07 +08:00
parent a55fd18915
commit 85ad31348e
8 changed files with 103 additions and 3 deletions
+41
View File
@@ -42,6 +42,47 @@
---
# Impact Analysis Report — 方案A:新话题禁用 dialog_context(防上下文污染)
## 1. 改动概览
- **背景与目标**:同一会话内出现多个不同话题时,历史摘要(含上轮 SQL)注入 Text2SQL 易造成上下文污染,导致选表/条件串话。本次引入“续问判定”规则:仅在疑似续问时注入少量上文;新话题默认不注入上文。
- **涉及模块**:`api_server.py`、`backend/utils/dialog_context.py`。
- **改动类型**:缺陷修复 / 行为优化(仅影响带 `session_id` 且存在历史消息的请求)。
## 2. 方法级改动分析
| 位置 | 变更 |
|------|------|
| `backend/utils/dialog_context.py` `is_likely_follow_up` | 新增:基于用户文本的轻量续问判定(关键字/短句规则),用于决定是否需要注入上文。 |
| `api_server.py` `_load_session_text2sql_context` | 调整:新增 `user_text` 入参;根据 `is_likely_follow_up(user_text)` 选择 `max_pairs=2` 或 `0`,并据此构造 `dialog_context`。 |
| `api_server.py` 两个入口 | 调整:`/g3sb/api/nl/chat` 与 `/g3sb/api/nl/chat/stream` 调用 `_load_session_text2sql_context(request, text)`。 |
## 3. 调用方与影响范围分析
- **调用方**:`/g3sb/api/nl/chat`、`/g3sb/api/nl/chat/stream`(请求带 `session_id` 且历史不为空时生效)。
- **行为变化**:
- **新话题**(非续问):`dialog_context` 变为空(不再携带历史摘要),降低串话风险。
- **续问**:仅携带最近 **2** 轮(原默认最多 8 轮),降低旧话题回流概率。
- **破坏性变更**:否(对外 API 入参/出参未变;仅内部上下文组装策略变化)。
## 4. 风险与回滚
- **风险级别**:中(可能把少量“隐式续问”误判为新话题,导致指代消解能力下降;但能显著降低跨话题污染)。
- **回滚**:回退本次提交(或将 `max_pairs` 固定恢复为 8 / 去掉续问判定)。
**回滚方式是否简单**:是。
## 5. 验证与测试
- 建议手工:
- 同一 session:话题A生成 SQL → 话题B(无续问词)应不再引用 A 的表/条件。
- 同一 session:首轮查询 → 次轮“再/按上面/沿用口径”续问,应仍能沿用上轮 SQL 的业务口径。
## 6. 配置变更
- 无。
# Impact Analysis Report — 库探针追问与交付分支(追加)
## 1. 改动概览
+32
View File
@@ -0,0 +1,32 @@
## 任务基本信息
- **任务**:方案A:同一聊天框多话题时避免上下文污染(新话题禁用 `dialog_context`)
- **项目**:`backman-camel`(FastAPI Text2SQL API)
- **范围**:后端接口 `/g3sb/api/nl/chat`、`/g3sb/api/nl/chat/stream` 的会话上文注入策略
## 改动说明
- **新增**:`backend/utils/dialog_context.py` 增加 `is_likely_follow_up(user_text)`,用于轻量判断是否为续问/沿用口径。
- **调整**:`api_server.py` `_load_session_text2sql_context(request, user_text)` 在构造上文时:
- **续问**:注入最近 2 轮会话摘要
- **新话题**:不注入历史摘要(`max_pairs=0`)
- **目的**:减少多话题情况下历史 SQL/条件回流导致的串话与错误 SQL。
## 影响与风险
- **破坏性变更**:否(对外 API 不变)
- **风险等级**:中
- 可能将少量“隐式续问”误判为新话题,导致指代消解能力下降
- 但能显著降低跨话题上下文污染导致的 SQL 错误
- **回滚**:回退本次改动即可恢复旧策略(或将 `max_pairs` 恢复为默认值 8)
## 测试与验证
- 建议手工验证:
- 同一 session:先做话题A数据查询,再提一个完全不同话题B(无“再/按上面”等续问词),应不再引用 A 的表/过滤条件。
- 同一 session:首轮查询后,第二轮使用“再/按上面/沿用口径”等续问词,应仍能续用上轮口径生成 SQL。
## 后续事项
- 若误判率偏高,可迭代 `is_likely_follow_up` 规则(补充关键词/短语),或升级为轻量 LLM 判别器(方案B)。
Binary file not shown.
+7 -3
View File
@@ -35,6 +35,7 @@ from utils.dialog_classifier import DialogIntent, classify_dialog
from utils.dialog_context import (
last_assistant_was_data_query,
messages_to_text2sql_context,
is_likely_follow_up,
)
logging.basicConfig(
@@ -246,6 +247,7 @@ def _conversation_nl_dict(reply: str) -> Dict[str, Any]:
async def _load_session_text2sql_context(
request: NLChatRequest,
user_text: str,
) -> tuple[str, bool]:
"""
在写入本轮之前读取会话历史,构造 Text2SQL 上文,并判断上一轮助手是否为数据查询。
@@ -266,7 +268,9 @@ async def _load_session_text2sql_context(
if not items:
return "", False
last_data = last_assistant_was_data_query(items)
block, _n = messages_to_text2sql_context(items)
# 方案A:仅在“续问/沿用口径”时注入少量上文;新话题直接清空,避免上下文污染 SQL。
max_pairs = 2 if is_likely_follow_up(user_text) else 0
block, _n = messages_to_text2sql_context(items, max_pairs=max_pairs)
return block, last_data
@@ -377,7 +381,7 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]:
return
text = request.message.strip()
orch = get_orchestrator()
dialog_block, last_data = await _load_session_text2sql_context(request)
dialog_block, last_data = await _load_session_text2sql_context(request, text)
classified = await asyncio.to_thread(
classify_dialog,
text,
@@ -461,7 +465,7 @@ async def nl_chat(request: NLChatRequest):
text = request.message.strip()
logger.info(f"[API] 问题: {text[:100]}...")
orch = get_orchestrator()
dialog_block, last_data = await _load_session_text2sql_context(request)
dialog_block, last_data = await _load_session_text2sql_context(request, text)
classified = await asyncio.to_thread(
classify_dialog,
text,
View File
+23
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import json
import logging
import re
from typing import Any, Dict, List, Tuple
logger = logging.getLogger(__name__)
@@ -14,6 +15,28 @@ _MAX_ASSISTANT_SQL_CHARS = 1400
_MAX_ASSISTANT_TEXT_CHARS = 900
_MAX_BLOCK_CHARS = 7500
# 续问/指代常见触发词:用于判定是否需要注入会话上文,避免多话题串扰。
# 规则应偏保守:宁可把新话题当作“无上文”也不要带入无关上下文污染 SQL。
_FOLLOW_UP_PATTERN = re.compile(
r"(再|继续|同样|沿用|还是|照旧|刚才|上面|上一条|上一个|上次|上述|这个SQL|这条SQL|按上面|按刚才|在此基础上|同一口径|同口径|按之前|改成|改为|加上|加一下|筛一下|过滤一下|补充|补一下|追加|优化一下|调整一下|换成|换为)",
re.IGNORECASE,
)
def is_likely_follow_up(user_text: str) -> bool:
"""
仅基于用户本轮文本做“是否续问”的轻量判断。
True -> 允许注入少量上文(最近 1~2 轮),用于指代消解/沿用口径
False -> 新话题,禁用上文,避免上下文污染
"""
t = (user_text or "").strip()
if not t:
return False
# 很短的“再来一个/同上/继续”等通常是续问
if len(t) <= 10 and _FOLLOW_UP_PATTERN.search(t):
return True
return bool(_FOLLOW_UP_PATTERN.search(t))
def _truncate(s: str, max_len: int) -> str:
s = (s or "").strip()