diff --git a/IMPACT_ANALYSIS.md b/IMPACT_ANALYSIS.md index 7a04970..1540f67 100644 --- a/IMPACT_ANALYSIS.md +++ b/IMPACT_ANALYSIS.md @@ -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. 改动概览 diff --git a/TASK_SUMMARY.md b/TASK_SUMMARY.md new file mode 100644 index 0000000..e8b7600 --- /dev/null +++ b/TASK_SUMMARY.md @@ -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)。 + diff --git a/__pycache__/api_server.cpython-312.pyc b/__pycache__/api_server.cpython-312.pyc index b8a4867..00a519a 100644 Binary files a/__pycache__/api_server.cpython-312.pyc and b/__pycache__/api_server.cpython-312.pyc differ diff --git a/api_server.py b/api_server.py index e94250f..7ac816a 100644 --- a/api_server.py +++ b/api_server.py @@ -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, diff --git a/backend/llm/openai_client.py b/backend/llm/openai_client.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/__pycache__/dialog_context.cpython-312.pyc b/backend/utils/__pycache__/dialog_context.cpython-312.pyc index 12aaefc..a12f9d8 100644 Binary files a/backend/utils/__pycache__/dialog_context.cpython-312.pyc and b/backend/utils/__pycache__/dialog_context.cpython-312.pyc differ diff --git a/backend/utils/dialog_context.py b/backend/utils/dialog_context.py index ad3d4b3..1b9209f 100644 --- a/backend/utils/dialog_context.py +++ b/backend/utils/dialog_context.py @@ -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() diff --git a/data/embeddings/chroma_fewshot/9756e6ea-6d51-4e24-a490-4a58f0a585b9/length.bin b/data/embeddings/chroma_fewshot/9756e6ea-6d51-4e24-a490-4a58f0a585b9/length.bin index a1bf4d6..e601c07 100644 Binary files a/data/embeddings/chroma_fewshot/9756e6ea-6d51-4e24-a490-4a58f0a585b9/length.bin and b/data/embeddings/chroma_fewshot/9756e6ea-6d51-4e24-a490-4a58f0a585b9/length.bin differ