From 695356a496cafaff149cae49fc9bce2e8e69fed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E8=BE=85=E5=85=83?= <2401926342@qq.com> Date: Thu, 16 Apr 2026 10:53:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=97=A5=E5=BF=97=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 - IMPACT_ANALYSIS.md | 41 ++ __pycache__/api_server.cpython-312.pyc | Bin 48712 -> 50918 bytes api_server.py | 72 +++- backend/__pycache__/main.cpython-312.pyc | Bin 18490 -> 18527 bytes .../__pycache__/orchestrator.cpython-312.pyc | Bin 31608 -> 33999 bytes backend/agents/orchestrator.py | 55 ++- .../deepseek_client.cpython-312.pyc | Bin 15475 -> 15512 bytes .../__pycache__/indexer.cpython-312.pyc | Bin 13499 -> 13946 bytes backend/schema/indexer.py | 30 +- .../dialog_classifier.cpython-312.pyc | Bin 10777 -> 11124 bytes .../fewshot_selector.cpython-312.pyc | Bin 25452 -> 27001 bytes .../__pycache__/repo_logging.cpython-312.pyc | Bin 0 -> 3717 bytes backend/utils/dialog_classifier.py | 24 +- backend/utils/repo_logging.py | 80 ++++ .../length.bin | Bin 4000 -> 4000 bytes logs/.gitkeep | 0 logs/text2sql_api.log | 67 +++ tech_architecture.md | 386 ++++++++++++------ 19 files changed, 582 insertions(+), 181 deletions(-) delete mode 100644 .idea/.gitignore create mode 100644 backend/utils/__pycache__/repo_logging.cpython-312.pyc create mode 100644 backend/utils/repo_logging.py create mode 100644 logs/.gitkeep create mode 100644 logs/text2sql_api.log diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/IMPACT_ANALYSIS.md b/IMPACT_ANALYSIS.md index d1866cb..74c01bc 100644 --- a/IMPACT_ANALYSIS.md +++ b/IMPACT_ANALYSIS.md @@ -551,3 +551,44 @@ | 项 | 说明 | |----|------| | `SSE_STREAM_CHUNK_CHARS` | 可选;每段 SSE 的 `content` 最大字符数,默认 `64`。 | + +--- + +# Impact Analysis Report — API 日志落盘与可观测性(追加) + +## 1. 改动概览 + +- **背景与目标**:终端日志偏少,难以排查 Text2SQL 全链路问题;需将更细粒度的日志写入仓库 `logs/`,且每次进程启动只保留一个 `.log` 文件。 +- **涉及模块**:`api_server.py`、`backend/utils/repo_logging.py`(新)、`backend/agents/orchestrator.py`、`backend/schema/indexer.py`、`backend/utils/dialog_classifier.py`。 +- **改动类型**:可观测性增强(行为对业务结果无影响)。 + +## 2. 方法级改动 + +| 位置 | 变更 | +|------|------| +| `configure_text2sql_api_logging` | 新建:清空 `logs/*.log`,创建 `logs/text2sql_api.log`(覆盖写);root 双 Handler(控制台简短格式 + 文件含 filename/lineno/funcName);关闭 Chroma/posthog 遥测 logger;`httpx`/`httpcore` 降为 WARNING。 | +| `api_server` 导入段 | 在 `from main import …` 之前调用上述配置;移除 `basicConfig`;`uvicorn.run(..., log_config=None)` 避免覆盖 root(勿用 `False`,否则会走 `fileConfig` 崩溃),并清空 uvicorn 自带 handler 改为 `propagate`。 | +| `_run_generate` / `nl_chat` / `_chat_stream_events` | 增加请求维度、对话上文长度、流式结束时的 valid/attempts/tables/SQL 摘要等 INFO 日志。 | +| `Text2SQLOrchestrator` | 向量粗筛输出表名+分数列表;选表输出 reasoning 预览;few-shot 注入 qid 与问题预览;生成 SQL 全文(超长截断至约 12k 字符);`_validate_sql` 返回前汇总 valid/errors/warnings/探针。 | +| `SchemaIndexer.search` | 检索结果由 DEBUG 改为 INFO,输出命中数与 top 表+分。 | +| `classify_dialog` 规则/hybrid 快路径 | DEBUG 改为 INFO,附用户输入预览;LLM 分支补充 reply 预览。 | + +## 3. 调用方与影响范围 + +- **调用方**:仅通过 `python api_server.py`(或等价导入 `api_server`)启动 API 时生效;`backend/main.py` CLI 仍使用自身 `basicConfig`,不落盘到本 `logs/text2sql_api.log`(未改 CLI)。 +- **破坏性变更**:否。 + +## 4. 风险与回滚 + +- **风险级别**:低。日志文件可能含用户查询片段与 SQL,需注意磁盘与隐私(内网演示场景可接受)。 +- **回滚**:删除 `repo_logging` 调用与相关增强日志,恢复 `basicConfig` 与默认 `uvicorn.run` 即可。 + +**回滚方式是否简单**:是。 + +## 5. 验证与测试 + +- 建议:`python -m py_compile api_server.py backend/utils/repo_logging.py`;启动 `python api_server.py` 后确认生成 `logs/text2sql_api.log` 且重启后仅保留该文件。 + +## 6. 配置变更 + +- 无新增环境变量;日志路径固定为仓库根下 `logs/text2sql_api.log`。 diff --git a/__pycache__/api_server.cpython-312.pyc b/__pycache__/api_server.cpython-312.pyc index 6600344d0f462d6317ece0a8b29f08b47c620941..9ad4c41f3d1e0fe53a7fc4bd446f4fbafa1cfef5 100644 GIT binary patch delta 14299 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 delta 12549 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 300 else ""), + ) async def _call_with_orch(o) -> GenerationResult: def _call() -> GenerationResult: @@ -643,6 +651,20 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]: text = request.message.strip() orch = get_orchestrator() dialog_block, last_data = await _load_session_text2sql_context(request, text) + logger.info( + "[API/stream] 开始: user_id=%r visitor_biz_id=%r session_id=%r service_code=%r model=%r " + "lang_code=%r msg_chars=%s preview=%r dialog_context_chars=%s last_turn_was_data_query=%s", + request.user_id, + request.visitor_biz_id, + request.session_id, + request.service_code, + request.model, + request.lang_code, + len(text), + text[:400] + ("…" if len(text) > 400 else ""), + len(dialog_block) if dialog_block else 0, + last_data, + ) lang = _normalize_lang_code(request.lang_code) async with _maybe_override_orch_llm(orch, request) as o: classified = await asyncio.to_thread( @@ -656,7 +678,11 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]: reply = (classified.reply_suggestion or "").strip() if not reply: reply = _localized_conversation_reply(lang) - logger.info("[API/stream] 对话意图: conversation(跳过 Text2SQL,与 CLI single_query 一致)") + logger.info( + "[API/stream] 对话意图 conversation(跳过 Text2SQL): reply_chars=%s reply_preview=%r", + len(reply), + reply[:500] + ("…" if len(reply) > 500 else ""), + ) yield _sse_data({"stage": "orchestrator", "stream_kind": "content", "content": "CHAT"}) async for pkt in _sse_stream_text_chunks("chat", reply): yield pkt @@ -669,10 +695,24 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]: try: result = await _run_generate(text, dialog_context=dialog_block or None, request=request) except Exception as e: - logger.error(f"[API/stream] 生成异常: {e}") + logger.error(f"[API/stream] 生成异常: {e}", exc_info=True) yield _sse_data({"code": 500, "msg": str(e), "data": None}) return + sql_out = (result.sql or "").strip() + logger.info( + "[API/stream] Text2SQL 完成: valid=%s attempts=%s tables_used=%s sql_chars=%s sql_head=%r", + result.valid, + result.attempts, + result.tables_used, + len(sql_out), + sql_out[:500] + ("…" if len(sql_out) > 500 else ""), + ) + if result.errors: + logger.warning("[API/stream] 错误列表: %s", result.errors) + if result.warnings: + logger.info("[API/stream] 警告列表: %s", result.warnings) + # What this SQL does / SQL 说明:按 lang_code 本地化 try: if isinstance(result.metadata, dict): @@ -747,9 +787,21 @@ async def nl_chat(request: NLChatRequest): lang = _normalize_lang_code(request.lang_code) raise HTTPException(status_code=400, detail=_localized_empty_input_reply(lang)) text = request.message.strip() - logger.info(f"[API] 问题: {text[:100]}...") orch = get_orchestrator() dialog_block, last_data = await _load_session_text2sql_context(request, text) + logger.info( + "[API/chat] 请求: user_id=%r session_id=%r service_code=%r model=%r lang=%r " + "msg_chars=%s preview=%r dialog_context_chars=%s last_turn_was_data_query=%s", + request.user_id, + request.session_id, + request.service_code, + request.model, + request.lang_code, + len(text), + text[:400] + ("…" if len(text) > 400 else ""), + len(dialog_block) if dialog_block else 0, + last_data, + ) lang = _normalize_lang_code(request.lang_code) async with _maybe_override_orch_llm(orch, request) as o: classified = await asyncio.to_thread( @@ -1004,5 +1056,7 @@ if __name__ == "__main__": host=host, port=port, reload=False, - log_level="info" + log_level="info", + # 必须为 None:False 仍会进入 uvicorn 的 fileConfig 分支并崩溃;None 才跳过覆盖 root 日志 + log_config=None, ) diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index d6f118d693088f1e2dfb3626ea6bbe7c723ecc8d..f4d4c9a98dbd25a4872443a933bf2d9b7dc80ef8 100644 GIT binary patch delta 74 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 200 else ""), + ) results = indexer.search( query=question, top_k=top_k, @@ -190,7 +194,15 @@ class Text2SQLOrchestrator: ) candidate_tables = [r["table_name"] for r in results] - logger.debug(f"粗筛候选表:{candidate_tables[:10]}...(共{len(candidate_tables)}个)") + scored = [ + (r["table_name"], round(float(r.get("score", 0.0)), 4)) + for r in results[: min(25, len(results))] + ] + logger.info( + "[Orchestrator] 向量粗筛: 命中=%s 张(阈值内),表名+分: %s", + len(candidate_tables), + scored, + ) return candidate_tables except Exception as e: @@ -239,7 +251,13 @@ class Text2SQLOrchestrator: # 限制数量 relevant_tables = relevant_tables[:max_tables] - logger.info(f"LLM精筛选中表:{relevant_tables}") + rs = (reasoning or "").strip() + logger.info( + "LLM精筛选中表:%s | reasoning_chars=%s reasoning_preview=%r", + relevant_tables, + len(rs), + rs[:600] + ("…" if len(rs) > 600 else ""), + ) return relevant_tables, reasoning _BROKER_KEYWORDS_CN = ("对手方", "经纪商", "券商", "對手方") @@ -361,7 +379,12 @@ class Text2SQLOrchestrator: for i, ex in enumerate(examples) ]) schema_str = f"参考以下相似示例的SQL编写风格:\n\n{examples_prompt}\n\n【当前Schema】\n{schema_str}" - logger.debug(f"已注入 {len(examples)} 个few-shot示例: {[ex.qid for ex in examples]}") + logger.info( + "已注入 %s 个 few-shot 示例: qid=%s question_zh_preview=%r", + len(examples), + [ex.qid for ex in examples], + [((ex.question_zh or "")[:80] + "…") if len(ex.question_zh or "") > 80 else (ex.question_zh or "") for ex in examples], + ) except Exception as e: logger.warning(f"Few-shot检索失败: {e}") @@ -419,7 +442,9 @@ class Text2SQLOrchestrator: sql = normalize_sql_for_dialect(sql, dialect) - logger.debug(f"生成的SQL:{sql[:200]}...") + lim = 12000 + body = sql if len(sql) <= lim else sql[:lim] + "\n…(日志已截断)" + logger.info("生成的SQL(chars=%s):\n%s", len(sql), body) return sql def _validate_sql( @@ -562,6 +587,19 @@ class Text2SQLOrchestrator: empty_feedback = (empty_feedback or prefix) + follow is_valid = len(errors) == 0 + logger.info( + "[validate] 程序+探针+LLM 汇总: valid=%s err_count=%s warn_count=%s " + "db_execution_status=%s sql_chars=%s", + is_valid, + len(errors), + len(warnings), + db_execution_status, + len(sql or ""), + ) + if errors: + logger.info("[validate] errors 预览: %s", errors[:5]) + if warnings: + logger.info("[validate] warnings: %s", warnings[:5]) return is_valid, errors, warnings, db_execution_status, empty_feedback def generate( @@ -608,7 +646,12 @@ class Text2SQLOrchestrator: dc_raw = (dialog_context or "").strip() retrieval_question = self._merge_dialog_for_model(dc_raw, question, 4000) linker_question = self._merge_dialog_for_model(dc_raw, question, 6000) - logger.info(f"[GEN] 开始生成SQL:{question[:50]}...") + logger.info( + "[GEN] 开始生成SQL: question_chars=%s preview=%r dialog_context_chars=%s", + len(question or ""), + (question or "")[:300] + ("…" if len(question or "") > 300 else ""), + len(dc_raw) if dc_raw else 0, + ) attempt = 0 last_sql = None diff --git a/backend/llm/__pycache__/deepseek_client.cpython-312.pyc b/backend/llm/__pycache__/deepseek_client.cpython-312.pyc index 8abd54eae1e06c685d80fb01555d66eaa5e5af99..ed78f27c96bae27761a24009e66afc5d9dccf28d 100644 GIT binary patch delta 76 zcmexdF{6_EG%qg~0}uq6J=n;t!(!y@Y!wq)oLW>IV`O4rXc^;@TAW>yUl5a&n4F!P bn5UbZn46jd=B4JP#N_1U#%y+B(YF8qvWXd( delta 39 tcmbPH`MHAoG%qg~0}xoc-rvZr!@?_@-0s!$Z3*7(! diff --git a/backend/schema/__pycache__/indexer.cpython-312.pyc b/backend/schema/__pycache__/indexer.cpython-312.pyc index 90af973db06aa2a4871ff069ff0865b843643136..186a4086ae26d38fb1b5358e86f7fec051098c62 100644 GIT binary patch delta 1693 zcmZ{lZERCj7{|}K?Y+Hyy{0w6A)_It?S0N&U;q*ke&7mAe2E&*>C!<=Jjwl? z|9#GLp65BI=iGkL^k<9dw$Z2qxZT=1+f?SgWMb4*n+oNr*Ps)$4S6lravt*VJT*!Y zF90BjtKuN2UVH;U(q~n(0rgm{YF<4`p?^G1G;3g2@N@_0q@|oCA4^s}>LnW*paad! zdCe$Xw4P_P*>EO9)-$f|(MzsJZ&~j)7^ktHX=wyB7i9ILUMO3t0x4G8aazCp@r=4` zw8t~@dgP;B$Y;r|{O=|#1q03hyCmb|30U+k9YnW1RcQLObq!HL%%Bb!$p5u&Z3^gC~-FASp}*nW>px9;Pc z1cAeYG8GkNrZFsq+d5>rCAvQ>$ZSJq2EP}5r?sOo{2bCc%v?=-B-|PnLM_!TO}G$r zSKH6}#RhB)`B6+4Sobde`MQC!3^+>q)C0yDSMi8G<*MjY-(l_3tYM0^PqOwYwrG+q z8j&vY6Kv5P_8D|T=dD;Ufc)aAJpW{#eP+s7BC0?XCcz z(-=4MMhEtj2r^B|&#Jhw*@X{q?m83CWshI>Q1Irgr>WB@EgiGwVluNZ zMoV}iWCIAB^v`J3av%>?wCgDMHhUNnC-YUh@Vd5e&ESXUN8KB>Z+Y5QG~4o zJVxW9I{@?K(#4*`*q+&ScRDdN-#d8kO!wU7kDhkF9t#TxgAI|8AZ{!b-RYt6yH_q? zg%8yHa9_InT1NLv(RM5ZD-Po^3r5@t^#opmR{%ic?topUg=nme7ix*oN8rcsaOf|0 zh?C&~S%t%hq)xXR?JcQf#<3I=$>9{8rd8{7#ecJ7|owR^H|Ps$X;ycI~mM8yzy z)^;{_Zrj+7k9S;Lf2BI*txB!jd_9mVsrjCJexhzqpK8JsoMBCa`hNYip>D>&CFtY& zVN1$TK(zK~?XT5DHJ?v3~)i`?o{@ delta 1334 zcmYjQUrbY17{90Oy}iBt%U??^Pzy_`N+}>16A>NkfoPbU%aTRorEqVcP}+L#En`dF zG^nFwan3JpAp3*cOo%YDHSXsi>*Z+zuD~qL)OmIKcg28eDB~)hF=U&ZECMErWw&lQ*^18;?6Lm0^F&q zV3A~4GQeM6|Lc-*g1N}1O_!~zvs9EeEukWhqcls5_-7er)K%iocif~_l%ml}xXsoE z*k!Kdv1~3e;$$n;N&H1FF2e)w9<-NUsl_5$mW3rlF^`xR9KEG8&v&&Rj+^SBzlw+R zoYnqfH!DnVBI2kgY;nDW-QX(WWpfW}3Ra{r%$TRboAWEVpKfihPj9WwK3@6e(dzZh z^-ngg-^|S|`UVCjM%0mk0bdL3HP%8Cf7hV9VVQTsQ@$MagVW;aMmn#^ zsv3lgCdPdse6Voxke8i7}+ zsb$B+@&RM@*VK!vxDTWBr$glPUV9Hom^J`NLPfK>t=7iBb*%p-?E`Yd+@N$o!m}{w`Hb zq(`gwHs@D!SGC7?7U|4pr*oIqyHfN@+v}O2104Qd2M&=?XC-+sH5|0Vs6WCgb|Qi9 z4|6{fqP(iP8a$}$iI1^3KeAyEuDNGX3*>PEoDEKB7X5d8I3DgKvZU3SDLkzMoR|A@~lp9%*BRyp0Jq@7M`@SYDN9slqYU zHF+~-Jmaa&TZ&a~--7Am_jYMzJ`OX7M#6{LnupHvJWFg;C@H9Yo6YVJ^*Yj-q6wMMt335+3Sp& Lkg&s0EDZ2}T%T{Y diff --git a/backend/schema/indexer.py b/backend/schema/indexer.py index 3613ef4..e880615 100644 --- a/backend/schema/indexer.py +++ b/backend/schema/indexer.py @@ -22,12 +22,8 @@ class SchemaIndexer: 功能: 1. 为表结构构建向量索引 2. 基于问题的表检索 -<<<<<<< HEAD 3. 默认使用 Chroma ``PersistentClient``,数据落在 ``persist_dir``(与 ``VECTOR_DB_PATH`` 一致); 仅当环境变量 ``SCHEMA_INDEXER_EPHEMERAL=true`` 或构造参数 ``use_ephemeral=True`` 时使用内存客户端。 -======= - 3. Chroma 使用持久化(PersistentClient),向量与元数据写入 ``persist_dir``,进程重启后可复用 ->>>>>>> 9369563942d8de442508613191670123d5ae9d08 """ def __init__( @@ -43,11 +39,7 @@ class SchemaIndexer: Args: embedder: Embedding模型实例 -<<<<<<< HEAD - persist_dir: Chroma 持久化目录(默认与编排器 ``vector_db_path`` / ``VECTOR_DB_PATH`` 一致) -======= persist_dir: Chroma 持久化根目录(磁盘路径) ->>>>>>> 9369563942d8de442508613191670123d5ae9d08 collection_name: 集合名称 use_ephemeral: 为 True 时使用内存 Chroma;为 None 时读环境变量 SCHEMA_INDEXER_EPHEMERAL """ @@ -56,17 +48,15 @@ class SchemaIndexer: self.persist_dir.mkdir(parents=True, exist_ok=True) self.collection_name = collection_name -<<<<<<< HEAD env_ephemeral = os.getenv("SCHEMA_INDEXER_EPHEMERAL", "").lower() in ( "1", "true", "yes", -======= + ) # 持久化 Chroma:数据落盘至 persist_dir self.client = chromadb.PersistentClient( path=str(self.persist_dir), settings=ChromaSettings(anonymized_telemetry=False), ->>>>>>> 9369563942d8de442508613191670123d5ae9d08 ) self._chroma_ephemeral = bool(use_ephemeral) if use_ephemeral is not None else env_ephemeral @@ -90,13 +80,8 @@ class SchemaIndexer: ) logger.info( -<<<<<<< HEAD f"[OK] 初始化SchemaIndexer({backend_desc}): collection={self.collection_name}, " f"count={self.collection.count()}" -======= - f"[OK] 初始化SchemaIndexer(Chroma持久化): collection={self.collection_name}, " - f"path={self.persist_dir}" ->>>>>>> 9369563942d8de442508613191670123d5ae9d08 ) def ensure_index_for_schema( @@ -255,7 +240,14 @@ class SchemaIndexer: "rank": idx + 1, }) - logger.debug(f"检索 '{query[:50]}...' -> 找到{len(formatted)}个相关表(阈值={score_threshold})") + top = [(x["table_name"], round(float(x.get("score", 0.0)), 4)) for x in formatted[:15]] + logger.info( + "Schema 向量检索: query_chars=%s 命中=%s(阈值=%s)top=%s", + len(query or ""), + len(formatted), + score_threshold, + top, + ) return formatted def search_by_table_names(self, table_names: List[str]) -> List[Dict]: @@ -332,9 +324,5 @@ class SchemaIndexer: "total_columns": total_columns, "avg_columns": total_columns / count if count > 0 else 0, "persist_dir": str(self.persist_dir), -<<<<<<< HEAD "chroma_mode": "memory" if self._chroma_ephemeral else "persistent", -======= - "chroma_mode": "persistent", ->>>>>>> 9369563942d8de442508613191670123d5ae9d08 } diff --git a/backend/utils/__pycache__/dialog_classifier.cpython-312.pyc b/backend/utils/__pycache__/dialog_classifier.cpython-312.pyc index 3df1ffe6cce6c2f65f262557ea69daaa0728252b..7153eed21d6c3ecf636d19939ef5cf7f8f0de4a4 100644 GIT binary patch delta 1672 zcmZuxO-vg{6y8~{jlDKtuuTmHdod>1qzyulLQj>U4u=>b{kVy zG)++&kps!}f~rWZ5>-`@s#aWjs!%0V)Lw?l2jo;Lt=dCwL~yLC&a6S0s4ML^Z{GXf zo7ws1?fUT1Xwff)g*-!^XE*l4y4vrG*12N)OduGMqGCfb5uT4XY(C>2^I8?*C>RDD zG75CBcm*_}_7aN zquz~lrB3w%C^gh2ucD3|6+OM&q6@#7^ zf-xwOz)}lwoiB3bahI;j6@E8Z3iG{9#9M6?U~*oH=hI!qDBv)nX5%F#7mm)xP~7;C zkC4?qAaA@K6`R^k+Iw1T?8k#Q@!Nhp;m3p18Fc{RDkEAjS+sJrcSp>qedvMdHG@3w z+ezW^Nqlp1XE~$xqn}LnT$NEg7Qi#JI5>;P<}&IKty0GEcYAL4V7+H6g$GCQ(8$(u zhIYH7Zc5qPKJT3SRMtUa)~5t zDA1I-$yBky=CXZES|eH_PQq$JZDyOcG7U(*Dp}|$a~1mATwi5PYd&Wmu_r5j3|uxZ zqy=1T)`q;*=FQIX9|7o~^0fB$_PGQoEzYO>!B7Ck-L>)K>8YOKSIOp+O;!nsMf4Z5fbb^LG-N0|U;x*-J_nxj~&9T<>Y0r~uUn?JXe{1;F z(Y$`~d;6}=eN@0{_#>XtJJ8cAdr{|B7!M5N!Qrh~MmMj1942O3lEqD9i@piOa&fNeQNeP&k^%zsFt}Am6vtMm4aW;Ri$E1W|&pkDCU4 zStFb~Ls^g#f&5}FRz6X-UhH-RSucq)K>GZ}Rd9o61mZYlB@m~Df`)27(2Ec3J|wp*0M zc5$$e?n4nzz^n>Ni6o4|hr~ky@~)^25e3=H=GiJJUptv0-#u<77MsrzUZEv;_z6u^ z6X<@43qPX~`5elrC9iOtbhhft%=_p{ofRN7S%(_rrL{DlrsfWuN|Y@K#ugUC^HP@Y zy5a2~@?2~8d0-IPoc0b`8d;Tonj>pAK`r{fgS1B=)c_hO7%7+tWYuj%De~XE0>9Y2 y<17cwm%*ERx{3n@8TX4z50qp)P&2lg0}Zu!#$-RxQcK6EDh>;&b(KZf)%71gAk6&$ delta 1299 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 E|J3y6y#N3J delta 3147 zcmZ{mdr(x@8NkoE`*0uZgU2o`k7XeUTopvEXjNF0hmQb?fRqJ9{hpEvp(~)4F*c#yv z)Qe{ScM|P_(47HCozrY$bimT>Yisvr%bVBBlGN7G%2vVyr^~vE@0%4MmLduvG1+0P zKyf+34F$>BS&LCz!Xc`e3+0svyfr{OylOtW_{AJ(T?VI<)3rV>dY|>*O3tCq+dA_B zW54loOx}p^hP&d5yW)ns=8C)Ks=M}@&f~97`AD6(25+t52pHBhwRg+KjCC^RL2E4( zrBBo5qv!@-dOlqa{`6Ej=)azRgGQHgdpX}$CHh0#DuDBu$x$eJ@vJ8IwE`Yyev6*- zr;C@gv;aPzIei(v5u1y^2L*(Vu7J9|vq@s1_m$D%Mm*h|zIc1H+~sTQ>|nL%F(UF| zT}~x!gST@s=mr1noYR5@?JE91zLjrbw*_9FGdEh0R68_;JV_zas{7&H1Qmmt4mg~ zM6N%{NWO)MtIvw|(7e0?oyo0eB<-e5zI$$mGwv8WSLbn+?yKUiDPMItC0$6R``Q#W zv-u(?*W6=ip^_fnDsZTFDh62Rh>25-{gxKt4PK{sQL^udnka^iHkkEnOu;U4%J|f! z7$l+y@uY|xFdwiqt6E93&@hF+FArI-x!0mt4hj48YSL>(&1dIDm>(U#N{Uc;^-HFB za)L-^Uqbjvy!fn>OLMzL5jd@iiZk-0e$vBh&sxbJLrJr$*V1d=M#LyMH!nQ}12&e! zAyW2zJ_OWqledTQXL4~v6O&-TV+VgxJ~Ym^(>~ZbzntpfH}g-WnE0!XW1red2DS`E z4*CXVhI<;O0o5kCr`rXZ1*xkt#+=;trG+CqJ3GU5_xZ!Fv7P<*M&A4qqj_)S^-o_r zckjKuu1sHNSL3FYRBKw**K+(d?XZp~n^UCdVvrm@1xH&BC zuUzZk#|v|(1i8c8K;h2|omN}iG21}Y@%Vw&S8bVl3^!rhq7{oB$C_Tme%SEal|e%5sr{xs;b6_NvY}~1iDxQ;Dr%bl zNTt#nf;wW2+hch=ZX_|eUM=77#oUOZxqe6fMr~|6SF1|cWe!cM^BVwZ!RMEOUER}lkIMz#&f5_(yQ-+nr zYqt;Y;E{bAYg)B{9)v|zN%RDGs;1JP!Op6A&t1gnM)rF?1XN8OJzt4XD2iCn;pUDD zvtJ{u2or~>3zt{O@CR-Pn0-yHt+LcOksjgjypQugKw!wcI*y4lDuCQ-H|_Utt-h|J zJD{w#(0B*uVa|*hc@8#a4GSTyoQ-muIFW<+cC3Np>p>CgX3;ZH>zN%D2v7gtlwKF} z37RRxa(=QHMApSp0KC@^b#-F;UG8PK5VvujArziaLkWKdCywg%y4kz~-l=mKu_o9( zL<7+JMf4Sae!Yjz7S-id<>jKlqPeSF)Uco9SkCr`R3W5}@kY;gG{$>95$*8B+O(ym zTx81-%MmLOZHO#{8lgvoDj<|yze5$zkX=J)5ci?IF@>Ik7aQZBOV!Nz*zP>$J+wKf z;c`$*V&Z~RP|^`w(y^*TM-Te6~iDI&RM5 zVsI^eMicjOlr5;?BVWuK9h`zvXtByeWkEe^1`_8QP!CiN>gQjHofb5rErK`_gC>;B zBsTe&Z=mpa&lS5Ew4ljK4Cc#b=M{bOcqHnpD6C!gspb($(b%A+=@a@nEL*>a>x7(# zYwI7RU^qpA|4i)ZlG#7FvLhlZi+?S$!e0ya0PZ&D3I8OJBF&_KgbK+)@4zO>NpHam zQaruQdsGY*lxyHUDZ1iq4BG$iQ|LL)ahn&EH~fh^UvYlRg{lk16HSXd$I!7ujhVee zSm0t8AXRRp7vXzyj&?n+Py{3L61olkEhj}{w+bKP9F!gr5L;H*un$arbNG+GzoF|s w>}knJ3nf@J>S>73p@$m{s+SNSz}1#qY6U}Uy{TJ6Z4U`D*k$y4TeZS}0UbUna{vGU diff --git a/backend/utils/__pycache__/repo_logging.cpython-312.pyc b/backend/utils/__pycache__/repo_logging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9adc8015fb67304b8301c59579fc1702ce862140 GIT binary patch literal 3717 zcmbtXTW}NC89pl=Nvo@6V*=*lEZB-IgXPeN!jLvhXkrWmNJFMAG#Po(u4NfXtM0D2 zXyh5ua*-PwiwR(J$&gH&#uOLRm&C=U%=D4IL}NA5?9iFapq0!MZgFQi=}Z43X_26A zc;L)v&wtMUpZ|9LbH1a$+U*tu?eLCo0)0R|AOo%G$^`ZJRUpO?hYli+;&eeOq&uji z2(1t559*=EK^!t1G{BsZGn~W+jmPzFQ?!vn$nqY4O#a^fAHU7K@y<>rKl71#`=jFR z&kJLpsdvX7&0a2?{aj6cp^n~SHZbbQh5U4)_?r{OE8~TWXYw;&Jeo}wrbY{spU&UC zQk?iuO--r4o+wP8Dqg?M1O>m;)!~7TQB!9ho*7Y3UwSk<^dRv|i=}w}k4$eb8wq$N zUOd8!y}gWjbwZt*RZo3Z7`tA$dTsvJuZrg<9?f3KPoD+R=#hX=5W^lZ8gBP^JR6C2 zuTKc~1^hqkW`h#%1{Duaji{;QquF6g{?1*dvV`ekx>-pk(G@SBEXP%x8D&I4keTAl zHHc_Fbw^F!Dn~@zdr0!?rW7k14hu3X2ZXS+7smJLicCHqKCb61oORN6qq4Vybew*X48rPlRqYxTH&ruIs$GqW)6bPKS0!p-)%2$(d>DjoR8sVj5sG~*a8}?&slHP)(_g^wx!P4z=moTG)k)qKmuLd z=6y+Gz`+B-0wd6^WQa2T)2k>^+K(=?|cz z6!2Fq!MACAcSDqgE6>6}gA2ZR=Vtlp1_1E$Vl6@e813BKy$|mH;>|?y+8+v^%&N1a z067}~KuTyN6ReYd5%dD`5Mb)V9m z@yf+AN)iww_VRZ>o}W4a7*)d96!?{&IR%r&3zuLrKmGeMpjEM}wCY`6j*4MvYiUqY zGd)u)Se*G-o%+-Kms9GWPZGc*A_^ucde5y>Oc7S(!?L8%{IP%}dxgU>Ln(}y^I5-R zn_TUzWIo7;cv(Ci zTmQdvo`@)LQJ?II2$I|{_#uI1!mU?Kl~+j2uDxJ*Q53|OLmRR_A7my{`t@C5~lSIlo6^YIb#;!(`qf*4|DnHLpnH{@EA zw1MI(%f4S0c{ZdmEiA`rwCSgByt{Yj-W>|f1tj)RkmraMo)r}{WGuq^VapZT>m{4$ z^(r_J?i1X0#Vqm>!AqWJrMSE#ZYrYC5TiE&4=@FncvQb)g@PfEKu8fNA_GzrimtC; z(fRrnLrJrOiGD>F^hZ?c247Sl?kj%ymWOKv3!`S+3;L6FIIlmtUAsk@wbC zzPBPG!ZnQ?@RQcTvn_!Z8j-VU{QK#;by-J8+R-uR*qGRn!`3rr<^`o##5gwlhg8N87UWsx-Z7d{c&A`$Y4Op&cVT z7fi^pZqb33t;#mEq#Igt^lLe1%c4QQ%$V4{NQ1>}9h0tOax`C_cN!Btixy<5&zjey z&1=SgHu0nBwv2guVtWo7&+I<2`@Ct)cFvZ;Ysq-uiG8Uapz~&D*4&&nH_w@w|1Y^a zgPCt26tZf22CpDtY4oBI8Ejd+JdKy9{ByWDi`&w;ZPGMVcfBryx7^3p3Ugo%ugl_& zH13$(JhkomwhVslu?{yFlZFKg>fAJJ%F<0~x@p`sM>Bt;-3wOWJ$9n{ZApCJ-k7yF zr|r$-@|?XjsmsxpQR|SkydXob&aGIT)Q#96XZ7142bw?U`{(e>S=^e&tzWq|Pp{12 z?K$hR#J+zo`YA}@KM`fFS8UD&J@o%sd*WL=oED4M+ZTmf9!`!(UYsI< za&mRc{lUPYl6a{+ucODnK;mY?rhfpJUNp$RE^dP%`6D5%h7Wj5QUA~xDEu!6qFNpx S$KTP)MdNmgY7}1w{(k|O7U;$R literal 0 HcmV?d00001 diff --git a/backend/utils/dialog_classifier.py b/backend/utils/dialog_classifier.py index e57bb4e..efa5b51 100644 --- a/backend/utils/dialog_classifier.py +++ b/backend/utils/dialog_classifier.py @@ -136,28 +136,28 @@ def _classify_dialog_rules( ) if _SQL_OR_QUERY_HINT_RE.search(t): - logger.debug("[dialog] intent=text2sql (query/business hint)") + logger.info("[dialog] intent=text2sql (query/business hint) preview=%r", user_text[:120]) return DialogClassifyResult(DialogIntent.TEXT2SQL, None) core = _strip_trailing_punct(t) if core.casefold() in _CHITCHAT_KEYS: - logger.debug("[dialog] intent=conversation (chitchat phrase)") + logger.info("[dialog] intent=conversation (chitchat phrase) core=%r", core) return DialogClassifyResult( DialogIntent.CONVERSATION, reply_suggestion=DEFAULT_CONVERSATION_REPLY ) if _META_QUESTION_RE.search(t): - logger.debug("[dialog] intent=conversation (meta question)") + logger.info("[dialog] intent=conversation (meta question) preview=%r", user_text[:120]) return DialogClassifyResult( DialogIntent.CONVERSATION, reply_suggestion=DEFAULT_CONVERSATION_REPLY, ) if last_turn_was_data_query: - logger.debug("[dialog] intent=text2sql (follow-up after data query)") + logger.info("[dialog] intent=text2sql (follow-up after data query) preview=%r", user_text[:120]) return DialogClassifyResult(DialogIntent.TEXT2SQL, None) - logger.debug("[dialog] intent=text2sql (default)") + logger.info("[dialog] intent=text2sql (default rules) preview=%r", user_text[:120]) return DialogClassifyResult(DialogIntent.TEXT2SQL, None) @@ -208,11 +208,15 @@ def _classify_dialog_llm( "若上一版 SQL 或结果不符合预期,请具体说明:希望增加/修改哪些条件、" "时间范围或统计维度,以便重新生成。" ) - logger.info("[dialog] intent=conversation (LLM)") + logger.info( + "[dialog] intent=conversation (LLM) reply_chars=%s reply_preview=%r", + len(reply), + reply[:300] + ("…" if len(reply) > 300 else ""), + ) return DialogClassifyResult(DialogIntent.CONVERSATION, reply_suggestion=reply) if intent_s == "text2sql": - logger.info("[dialog] intent=text2sql (LLM)") + logger.info("[dialog] intent=text2sql (LLM) user_preview=%r", user_text[:200]) return DialogClassifyResult(DialogIntent.TEXT2SQL, None) raise ValueError(f"unexpected intent field: {intent_s!r}") @@ -254,18 +258,18 @@ def classify_dialog( ) if _SQL_OR_QUERY_HINT_RE.search(t): - logger.debug("[dialog] intent=text2sql (query/business hint, hybrid fast)") + logger.info("[dialog] intent=text2sql (hybrid fast: query hint) preview=%r", user_text[:120]) return DialogClassifyResult(DialogIntent.TEXT2SQL, None) core = _strip_trailing_punct(t) if core.casefold() in _CHITCHAT_KEYS: - logger.debug("[dialog] intent=conversation (chitchat phrase, hybrid fast)") + logger.info("[dialog] intent=conversation (hybrid fast: chitchat) core=%r", core) return DialogClassifyResult( DialogIntent.CONVERSATION, reply_suggestion=DEFAULT_CONVERSATION_REPLY ) if _META_QUESTION_RE.search(t): - logger.debug("[dialog] intent=conversation (meta question, hybrid fast)") + logger.info("[dialog] intent=conversation (hybrid fast: meta) preview=%r", user_text[:120]) return DialogClassifyResult( DialogIntent.CONVERSATION, reply_suggestion=DEFAULT_CONVERSATION_REPLY, diff --git a/backend/utils/repo_logging.py b/backend/utils/repo_logging.py new file mode 100644 index 0000000..059f88b --- /dev/null +++ b/backend/utils/repo_logging.py @@ -0,0 +1,80 @@ +""" +Text2SQL API 仓库级日志:控制台 + 单一磁盘文件(每次进程启动清空 logs/*.log 后重写)。 + +由 ``api_server`` 在导入早期调用;与 ``uvicorn.run(..., log_config=False)`` 配合, +使 uvicorn / FastAPI 的日志经 root 统一落到文件。 +""" + +from __future__ import annotations + +import logging +from pathlib import Path + + +def configure_text2sql_api_logging(repo_root: Path) -> Path: + """ + 配置 root logger:控制台(INFO,简短格式)+ ``logs/text2sql_api.log``(INFO,含文件名/行号/函数)。 + + 每次调用会删除 ``logs`` 目录下所有 ``*.log``,再新建 ``text2sql_api.log``(覆盖写), + 保证一次运行仅保留一个日志文件。 + + Returns: + 主日志文件绝对路径。 + """ + log_dir = (repo_root / "logs").resolve() + log_dir.mkdir(parents=True, exist_ok=True) + for p in log_dir.glob("*.log"): + try: + p.unlink() + except OSError: + pass + + log_path = log_dir / "text2sql_api.log" + + root = logging.getLogger() + root.setLevel(logging.INFO) + for h in list(root.handlers): + root.removeHandler(h) + try: + h.close() + except Exception: + pass + + fmt_console = logging.Formatter( + "%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + fmt_file = logging.Formatter( + "%(asctime)s %(levelname)-7s [%(name)s] %(filename)s:%(lineno)d %(funcName)s() | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + fh = logging.FileHandler(log_path, mode="w", encoding="utf-8") + fh.setLevel(logging.INFO) + fh.setFormatter(fmt_file) + + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + ch.setFormatter(fmt_console) + + root.addHandler(fh) + root.addHandler(ch) + + # 依赖库降噪(避免 httpx 每条请求刷屏;Chroma 遥测与新版 posthog 不兼容时会打 ERROR) + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) + for _name in ( + "chromadb.telemetry", + "chromadb.telemetry.product.posthog", + "posthog", + ): + logging.getLogger(_name).disabled = True + + # Uvicorn 默认自带 handler;与 log_config=False 合用时改为只往 root 冒泡 + for name in ("uvicorn", "uvicorn.error", "uvicorn.access"): + lg = logging.getLogger(name) + lg.handlers.clear() + lg.propagate = True + + logging.getLogger(__name__).info("日志文件: %s", log_path) + return log_path 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 2d95673b7f364c362182bc2504462b316e93eda3..d33cd2cde2cc90752d8fce7e08ae86ec0ccc3530 100644 GIT binary patch literal 4000 zcma)M?t;{xRYDoN2V-sUrKln;CCg=y1Ncn)XcbOSD+y?gp=l?r% z&Y3fF=guyjf3|0rTM+868#U|&D>6Q_bDthWkQ#C=`0G{tw-`}bHTcmZSe0<-MM#e{ zudYX#`_)L38rECH&aGsAhHfJ^)jr=7(!+BuJ(|^WEFM>#i+v<|SP6M`I|HBy2b=l*ZmBrP;w^ z9}s1P&=sPXK0W#N$CB7aMGIpcSsX}r5~l+-D&APe!P{iDC?!Na(p_vMPdJ1iF1|iJ zjztK+6l|s>W9`x4I>h&iṋv+fgmcVj?^RM9< z(vmnH%{p{yhkOt7F?B6h()tRxR@ZSQEr}hT`>xOv+>s%24ngpA>;hP}gDy6;KOY6(};5}VsL63u&5pOzzBktnfB8(V?N$@gvHv1%Dt z(z2DrjoA5HVSHCEF^jL9XA|4J+S|BSTHZH1S2nd$`ne(zym#Yto7_CawjjdfyHZAM zo-5m_l$Bc0uCQU`MacW>P$lZWK4JXMDY({Z*s`+FcqoCzZw34eVD@7vSWm=n16%=k zJK##dI{@zl>;}9Ga24QZ0apXw4VYaT3)WKucn{#cfIWa~0oMV34zLE;3)lyEAK-ex z4S*W~`vE@>_yxcOunu@X;3mM$fDZtE5pWCOR=_U-ei`s9fL{gN2DlyYLBOv8ejV@| zfI9$p0)7*47vMvHy8*ui_%Prez()Wd1$+$f+klS)eg|+b;6A|pfZqlD9^eyz-v@jW z@CSfT0UiK62zUrEyQml32P1$#1Uw4(v^&hLw1%^rbd0@fIN+beD>DmmDnhB#!rdm|Keo`q-kam2-gbI` zJ>B^1%vHSayK{8)kz{=QGb=td^$nf$xdPt~rsuEBoS=i}9}1N8{!E*!k+jwJ+qk>8 zh|ZM%MWdSk#y{jZ1HTM*;0;HA$}8Pq2~<^lOz->sFpjod#wR>&_}Yc1KJ_<5?`ac@ixoyaARc)u|ZjF43`aX^8F|NyYyTT_1lW`wVVKZ^HipEBZcn literal 4000 zcmeH_Jx;?w5QPn8Dvl7j#%O-_my;l*2pPqS5J?;ngODI_1xgOV1?V^l9X$;_vr9zQ z<^VLih1Ys|`)20beR@B2uX{!D`SBR{(RjPy2ndOdHhB0eR-ytbQ0!3mWyO}2?Q2BF z7#01ki>>HG=?j?0HxUy+`Mf`(8i;Oj^@G)}WaQR9{AA4Jh-+%vRdt#2hA; zV3e5?HG=TUYNrQy2uLAgGDt{pGMYsc;1`&xQAqj}F3N2fLq0Hpv=B?|OV;?R)=`8f%SUIo!&KYwZAOdZl%_Qy{ z`A&kZT(r|>ULKFjuDR{1W^vw3rx(jvRZr^*fT|lRqwjiM&l>UF#q;Irs$HzQv#z~t zmTQr5TeR%juAZm5_&1}?s7TlSe(~_4o{P8oB);>5ub+3Hdh&lJkO^c0nLs9x31kA9 NKqin0WCH(5;2Tr`juij^ diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/text2sql_api.log b/logs/text2sql_api.log new file mode 100644 index 0000000..d59031f --- /dev/null +++ b/logs/text2sql_api.log @@ -0,0 +1,67 @@ +2026-04-16 10:32:14 INFO [utils.repo_logging] repo_logging.py:79 configure_text2sql_api_logging() | 日志文件: C:\Users\24019\Desktop\backman-camel\logs\text2sql_api.log +2026-04-16 10:32:17 INFO [__main__] api_server.py:86 () | [OK] 已加载配置文件: C:\Users\24019\Desktop\backman-camel\.env +2026-04-16 10:32:17 INFO [__main__] api_server.py:1047 () | 启动服务: http://0.0.0.0:8041 +2026-04-16 10:32:17 INFO [__main__] api_server.py:1048 () | API文档: http://0.0.0.0:8041/docs +2026-04-16 10:32:17 INFO [uvicorn.error] server.py:92 _serve() | Started server process [6728] +2026-04-16 10:32:17 INFO [uvicorn.error] on.py:48 startup() | Waiting for application startup. +2026-04-16 10:32:17 INFO [__main__] api_server.py:741 lifespan() | ============================================================ +2026-04-16 10:32:17 INFO [__main__] api_server.py:742 lifespan() | Text2SQL API Server 启动中... +2026-04-16 10:32:17 INFO [__main__] api_server.py:743 lifespan() | ============================================================ +2026-04-16 10:32:17 INFO [main] main.py:126 setup_environment() | [OK] 环境检查通过 +2026-04-16 10:32:17 INFO [main] main.py:127 setup_environment() | - Schema: data\schemas\G3SB_MCDataDictionary_table_structure.json +2026-04-16 10:32:17 INFO [main] main.py:128 setup_environment() | - LLM: openai +2026-04-16 10:32:17 INFO [main] main.py:154 load_schema() | 加载Schema: ./data/schemas/G3SB_MCDataDictionary_table_structure.json +2026-04-16 10:32:17 INFO [main] main.py:156 load_schema() | 表注释(meta): ./data/schemas/G3SB_MCDataDictionary_table_meta.json +2026-04-16 10:32:18 INFO [schema.loader] loader.py:107 load_from_json() | [OK] 加载Schema完成(G3SB schemas): G3SB_MCDataDictionary_table_structure, 共2516张表 +2026-04-16 10:32:18 INFO [main] main.py:163 load_schema() | [OK] Schema加载完成: G3SB_MCDataDictionary_table_structure, 共2516张表, 59196个字段 +2026-04-16 10:32:21 INFO [llm.openai_client] openai_client.py:55 __init__() | [OK] OpenAIClient初始化: model=gpt-5.4, base_url=http://113.192.49.54:9080/v1 +2026-04-16 10:32:23 INFO [utils.embedding] embedding.py:144 __init__() | 使用 OpenAI Embedding API:model=text-embedding-ada-002,base_url=http://113.192.49.54:9080/v1,max_batch=100 +2026-04-16 10:32:25 INFO [utils.fewshot_chroma_store] fewshot_chroma_store.py:86 __init__() | [OK] FewShotChromaStore: 磁盘 data\embeddings\chroma_fewshot collection=fewshot_samples count=50 +2026-04-16 10:32:25 INFO [utils.fewshot_selector] fewshot_selector.py:128 _init_chroma_mode() | [Few-shot] 已从向量库加载(Chroma 50 条,data\embeddings\chroma_fewshot) +2026-04-16 10:32:25 INFO [utils.fewshot_selector] fewshot_selector.py:159 _init_chroma_mode() | [OK] Few-shot 使用 Chroma(50 条) +2026-04-16 10:32:25 INFO [agents.orchestrator] orchestrator.py:116 __init__() | Few-shot已启用: top_k=3, min_rating=7 +2026-04-16 10:32:25 INFO [agents.orchestrator] orchestrator.py:124 __init__() | [OK] Text2SQLOrchestrator初始化完成: max_retry=2, use_vector_search=True, fewshot=on, nl→zh_norm=on +2026-04-16 10:32:25 INFO [__main__] api_server.py:133 get_orchestrator() | [OK] Orchestrator 初始化完成 +2026-04-16 10:32:25 INFO [__main__] api_server.py:747 lifespan() | [OK] 服务已就绪 +2026-04-16 10:32:25 INFO [uvicorn.error] on.py:62 startup() | Application startup complete. +2026-04-16 10:32:25 INFO [uvicorn.error] server.py:224 _log_started_message() | Uvicorn running on http://0.0.0.0:8041 (Press CTRL+C to quit) +2026-04-16 10:32:42 INFO [uvicorn.access] httptools_impl.py:483 send() | 127.0.0.1:56978 - "POST /g3sb/api/nl/chat/stream HTTP/1.1" 200 +2026-04-16 10:32:42 INFO [__main__] api_server.py:654 _chat_stream_events() | [API/stream] 开始: user_id='anonymous' visitor_biz_id=None session_id=None service_code=None model='gpt-4o-mini' lang_code='auto' msg_chars=15 preview='所有客户账户之间的股票转移记录' dialog_context_chars=0 last_turn_was_data_query=False +2026-04-16 10:32:45 INFO [llm.openai_client] openai_client.py:55 __init__() | [OK] OpenAIClient初始化: model=gpt-4o-mini, base_url=http://113.192.49.54:9080/v1 +2026-04-16 10:32:45 INFO [utils.dialog_classifier] dialog_classifier.py:261 classify_dialog() | [dialog] intent=text2sql (hybrid fast: query hint) preview='所有客户账户之间的股票转移记录' +2026-04-16 10:32:45 INFO [__main__] api_server.py:620 _run_generate() | [GEN/API] dialect=tsql top_k=20 dialog_context_chars=0 question_len=15 preview='所有客户账户之间的股票转移记录' +2026-04-16 10:32:48 INFO [llm.openai_client] openai_client.py:55 __init__() | [OK] OpenAIClient初始化: model=gpt-4o-mini, base_url=http://113.192.49.54:9080/v1 +2026-04-16 10:32:51 INFO [agents.orchestrator] orchestrator.py:636 generate() | [GEN] 问句已归一中文:所有客户账户的股票转移记录 +2026-04-16 10:32:51 INFO [agents.orchestrator] orchestrator.py:649 generate() | [GEN] 开始生成SQL: question_chars=13 preview='所有客户账户的股票转移记录' dialog_context_chars=0 +2026-04-16 10:32:51 INFO [agents.orchestrator] orchestrator.py:664 generate() | 尝试 #1 +2026-04-16 10:32:51 INFO [schema.indexer] indexer.py:82 __init__() | [OK] 初始化SchemaIndexer(Chroma磁盘 path=data\embeddings\chroma): collection=schema_tables, count=2516 +2026-04-16 10:32:51 INFO [schema.indexer] indexer.py:106 ensure_index_for_schema() | Schema 向量索引已就绪(2516 张表),跳过向量化 +2026-04-16 10:32:51 INFO [agents.orchestrator] orchestrator.py:185 _coarse_filter() | [Orchestrator] 开始向量检索: query_chars=13 query_preview='所有客户账户的股票转移记录' +2026-04-16 10:32:53 INFO [utils.embedding] embedding.py:163 _set_dim_from_vector() | [OK] Embedding 向量维度:1536 +2026-04-16 10:32:53 INFO [schema.indexer] indexer.py:244 search() | Schema 向量检索: query_chars=13 命中=20(阈值=0.1)top=[('VSBHKRpt0430', 0.8264), ('VSBHKRpt0431', 0.8188), ('TSBTransferInstruction', 0.8147), ('VSBTransferInstruction', 0.8134), ('VSBHKRpt0397', 0.8126), ('VSBHKRpt0672', 0.8117), ('VSBHKRpt0090C', 0.81), ('VSBHKRpt0570', 0.8084), ('VSBHKRpt1030A', 0.8077), ('VSBRpt0999E', 0.8065), ('VSBRpt0999C', 0.8064), ('TSBAccountEntitlementRelease', 0.8063), ('VSBRpt0999A', 0.8059), ('TSBAccountInstrumentMovement', 0.8038), ('VSBTransferInstructionGenerationByAccountContract', 0.8033)] +2026-04-16 10:32:53 INFO [agents.orchestrator] orchestrator.py:201 _coarse_filter() | [Orchestrator] 向量粗筛: 命中=20 张(阈值内),表名+分: [('VSBHKRpt0430', 0.8264), ('VSBHKRpt0431', 0.8188), ('TSBTransferInstruction', 0.8147), ('VSBTransferInstruction', 0.8134), ('VSBHKRpt0397', 0.8126), ('VSBHKRpt0672', 0.8117), ('VSBHKRpt0090C', 0.81), ('VSBHKRpt0570', 0.8084), ('VSBHKRpt1030A', 0.8077), ('VSBRpt0999E', 0.8065), ('VSBRpt0999C', 0.8064), ('TSBAccountEntitlementRelease', 0.8063), ('VSBRpt0999A', 0.8059), ('TSBAccountInstrumentMovement', 0.8038), ('VSBTransferInstructionGenerationByAccountContract', 0.8033), ('VSBRpt0060', 0.8021), ('XCGatewayStockReconciliationReport', 0.802), ('TSBAccountEntitlementHold', 0.8013), ('WSBBatchLocationTransferDetail', 0.8012), ('VCAccountCashMovement', 0.8012)] +2026-04-16 10:32:57 INFO [agents.orchestrator] orchestrator.py:255 _llm_select_tables() | LLM精筛选中表:['VSBHKRpt0430', 'VSBHKRpt0431', 'TSBTransferInstruction', 'VSBTransferInstruction', 'TSBAccountInstrumentMovement'] | reasoning_chars=175 reasoning_preview='问题涉及客户账户的股票转移记录,VSBHKRpt0430和VSBHKRpt0431提供了客户股票转移的日报信息,TSBTransferInstruction和VSBTransferInstruction记录账户转移指令,TSBAccountInstrumentMovement则详细记录账户的股票移动交易。这些表共同涵盖了客户账户的股票转移相关信息。' +2026-04-16 10:32:57 INFO [agents.orchestrator] orchestrator.py:692 generate() | 选中表:['VSBHKRpt0430', 'VSBHKRpt0431', 'TSBTransferInstruction', 'VSBTransferInstruction', 'TSBAccountInstrumentMovement'],扩展后:['VSBHKRpt0430', 'VSBHKRpt0431', 'TSBTransferInstruction', 'TSBAccountInstrumentMovement', 'VSBTransferInstruction'] +2026-04-16 10:32:58 INFO [utils.fewshot_selector] fewshot_selector.py:271 _select_chroma() | Few-shot(Chroma)选择: 问题='所有客户账户的股票转移记录...' → 选中3个示例 (top_k=3, min_rating=7) +2026-04-16 10:32:58 INFO [agents.orchestrator] orchestrator.py:382 _generate_sql() | 已注入 3 个 few-shot 示例: qid=['Q2', 'Q40', 'Q22'] question_zh_preview=['列出今日所有客户账户之间的股票转移记录。', '列出今日执行的所有股票转移/移动。', '列出今日所有公司账户与客户账户之间的现金划转。'] +2026-04-16 10:33:07 INFO [agents.orchestrator] orchestrator.py:447 _generate_sql() | 生成的SQL(chars=476): +SELECT + m.MovementID, + m.AccountID AS FromAccountID, + m.TransferToAccountID, + m.InstrumentID, + i.Name AS InstrumentSymbol, + m.MovementType, + m.Quantity AS TransferQuantity, + m.ValueDate AS TransferDate +FROM TSBAccountInstrumentMovement m +LEFT JOIN MCInstrument i ON m.InstrumentID = i.InstrumentID +WHERE m.MovementType = 'T' + AND m.ValueDate >= CAST(GETDATE() AS DATE) + AND m.ValueDate < DATEADD(DAY, 1, CAST(GETDATE() AS DATE)) +ORDER BY m.ValueDate; +2026-04-16 10:33:07 INFO [db.engine] engine.py:45 get_engine() | SQLAlchemy engine initialized from database_url +2026-04-16 10:33:07 INFO [db.dbhub_tools] dbhub_tools.py:627 _execute_sql() | _execute_sql 执行语句数=1 readonly=True max_rows=1 +2026-04-16 10:33:10 INFO [agents.orchestrator] orchestrator.py:590 _validate_sql() | [validate] 程序+探针+LLM 汇总: valid=True err_count=0 warn_count=0 db_execution_status=0 sql_chars=476 +2026-04-16 10:33:10 INFO [agents.orchestrator] orchestrator.py:753 generate() | [OK] SQL生成与验证通过(1次尝试) +2026-04-16 10:33:10 INFO [__main__] api_server.py:703 _chat_stream_events() | [API/stream] Text2SQL 完成: valid=True attempts=1 tables_used=['VSBHKRpt0430', 'VSBHKRpt0431', 'TSBTransferInstruction', 'TSBAccountInstrumentMovement', 'VSBTransferInstruction'] sql_chars=476 sql_head="SELECT\n m.MovementID,\n m.AccountID AS FromAccountID,\n m.TransferToAccountID,\n m.InstrumentID,\n i.Name AS InstrumentSymbol,\n m.MovementType,\n m.Quantity AS TransferQuantity,\n m.ValueDate AS TransferDate\nFROM TSBAccountInstrumentMovement m\nLEFT JOIN MCInstrument i ON m.InstrumentID = i.InstrumentID\nWHERE m.MovementType = 'T'\n AND m.ValueDate >= CAST(GETDATE() AS DATE)\n AND m.ValueDate < DATEADD(DAY, 1, CAST(GETDATE() AS DATE))\nORDER BY m.ValueDate;" diff --git a/tech_architecture.md b/tech_architecture.md index d6a2126..db6e0b4 100644 --- a/tech_architecture.md +++ b/tech_architecture.md @@ -22,79 +22,123 @@ Text2SQL 系统由以下三个核心智能体组成: flowchart TD subgraph 输入层 A[自然语言问题] + Actx[可选会话上文 +dialog_context] end - + subgraph 核心处理层 - A --> B[意图分类 -Dialog Classifier] - B -->|非查询意图| C[返回对话回复] - - subgraph "智能体 1: Schema Linker" - D[表选择] - D1[向量检索粗筛 -SchemaIndexer] - D2[LLM 精筛 -DeepSeek] - D3[外键扩展 -关联表处理] + A --> B[意图分类 classify_dialog] + Actx --> B + B -->|conversation| C[直接返回引导性中文回复] + B -->|text2sql| N0[问句归一 normalize_nl_question +TRANSLATE_EN_TO_ZH=true 默认开启] + N0 --> D0[合并上文用于检索选表 +_merge_dialog_for_model] + + subgraph "Schema Linker 表选择模块" + D[表选择流程] + D1[向量粗筛 _coarse_filter +SchemaIndexer Chroma 或全表降级] + D2[LLM 精筛 _llm_select_tables +select_tables] + D2b[经纪商维度表优先 +_prioritize_broker_tables] + D3[外键双向扩展 +_expand_relations] + D4[生成紧凑 Schema 字符串 +to_compact_string] D --> D1 D1 --> D2 - D2 --> D3 + D2 --> D2b + D2b --> D3 + D3 --> D4 end - - B -->|查询意图| D - - subgraph "智能体 2: SQL Generator" - E[SQL 生成] - E1[Few-shot 示例增强] - E2[LLM 生成 SQL -DeepSeek] + + D0 --> D + + subgraph "SQL Generator SQL生成模块" + E[SQL 生成 _generate_sql] + E1[Few-shot 示例增强 FewShotSelector +Chroma 或 JSONL FEWSHOT_USE_CHROMA] + E2[LLM chat 生成 +temperature=0.0 top_p=1.0] + E3[sqlglot 方言归一 +normalize_sql_for_dialect] E --> E1 E1 --> E2 + E2 --> E3 end - - D3 --> E - - subgraph "智能体 3: Validator" - F[SQL 验证] - F1[程序验证 -语法检查] - F1b[数据库试执行 -行列探针 0/1/-1] - F2[LLM 语义验证 -未探针时] - F3[结果评估] + + D4 --> E + + subgraph "Validator SQL验证模块 _validate_sql" + F[程序校验] + F1[语法验证 sqlglot] + F1a[表列一致性验证 SchemaManager] + F1b[危险操作检查 check_dangerous_operations] + F1c[T-SQL 字面量规则检查 +check_no_cjk_in_sql_string_literals] + F1d[库探针 probe_sql_execution_status_ex +需 database_url 配置] + F2[LLM 语义验证 validate_sql +仅探针为 None 时调用] + F0a[探针 1 交付说明 +sql_probe_success_delivery_message] + F0b[探针 0 无数据说明 +empty_result_user_feedback] F --> F1 - F1 --> F1b - F1b --> F2 - F2 --> F3 + F1 --> F1a + F1a --> F1b + F1b --> F1c + F1c -->|无程序错误| F1d + F1c -->|有程序错误| I[未通过 max_retry 内重试] + F1d -->|探针 None| F2 + F1d -->|探针 1| F0a + F1d -->|探针 0| F0b + F1d -->|探针 -1| I + F2 -->|LLM 报错| I + F2 -->|通过| H[GenerationResult 有效 SQL] + F0a --> H + F0b --> H end - - E2 --> F - F3 -->|验证通过| H[返回有效 SQL] - F3 -->|验证失败| I[重试逻辑 -最多 max_retry 次] - I -->|重试| E + + E3 --> F + + I --> R[重试回合] + R --> R1[extract_tables 合并上次SQL引用的表] + R1 --> R2[外键再扩展 _expand_relations] + R2 --> R3[重新生成紧凑 Schema to_compact_string] + R3 -->|下一 attempt| E end - + subgraph 数据层 - J[Schema 管理 -SchemaManager] - K[向量数据库 -Chroma] - L[Few-shot 示例库 -JSONL] - M[DeepSeek API -大语言模型] - N[业务数据库 + J[SchemaManager +加载 Schema 元数据] + K[Chroma 持久化 +schema 向量索引 SchemaIndexer] + K2[Few-shot 向量库或 JSONL +FewShotSelector] + Emb[嵌入模型 +OpenAI 兼容 Embeddings API] + M[DeepSeek LLM +统一接口 deepseek.chat/select_tables +/validate_sql 等方法] + N[业务库 SQLAlchemy database_url] - J --> D + J --> D1 + J --> F1a + Emb --> D1 + Emb --> E1 K --> D1 - L --> E1 + K2 --> E1 + M --> N0 M --> D2 M --> E2 M --> F2 - N --> F1b + M --> F0a + M --> F0b + M --> B + N --> F1d end ``` @@ -103,66 +147,115 @@ database_url] ### 1. 意图分类 - **Dialog Classifier**:对用户输入的自然语言进行意图分类 -- **分类结果**:非查询意图直接返回对话回复,查询意图进入 SQL 生成流程 +- **分类模式**:支持 `rules`(仅规则)和 `hybrid`(混合)两种模式 + - `rules` 模式:仅使用关键词和短语规则,无 LLM 调用 + - `hybrid` 模式(默认):明显查数词/寒暄/元问题走规则快速返回;其余交 LLM 判断 +- **分类结果**:非查询意图(conversation)直接返回对话回复,查询意图(text2sql)进入 SQL 生成流程 +- **规则关键词**:包含查数词(查、查询、统计、汇总)、业务词(余额、交易、账户、持仓)、寒暄词(元问题、致谢等) -### 2. Schema Linker 算法 +### 2. Schema Linker 表选择模块 -- **向量检索粗筛**:使用 `SchemaIndexer` 对用户问题进行向量检索,获取相关表的候选列表 - - 利用预训练的嵌入模型将表名和描述转换为向量 +- **问句归一化(可选)**:`normalize_nl_question_for_text2sql` + - 将用户中英文问题归一为标准中文(默认开启,`TRANSLATE_EN_TO_ZH=true`) + - 使同一语义的中英文表述对齐,保证 SQL 一致性 + - 使用 LLM temperature=0 贪心解码 + +- **对话上文合并**:`_merge_dialog_for_model` + - 拼接上文与当前问句,控制总长度(默认 4000 字符用于检索,6000 字符用于选表) + - 优先保留当前问句完整 + +- **向量检索粗筛**:`_coarse_filter` → `SchemaIndexer` + - 使用嵌入模型将表结构转换为向量 - 使用 Chroma DB 进行相似度搜索 - - 返回 top-k 个最相关的表 + - 返回 top-k 个最相关的候选表 + - 可配置是否启用向量检索(`use_vector_search`) -* **LLM 精筛**:调用 DeepSeek 模型对候选表进行精筛,选择最相关的表 - - 构造包含表名和描述的提示 - - 让 LLM 基于用户问题选择最相关的表 -* **外键扩展**:自动添加与选中表相关的关联表,确保查询完整性 - - 分析表之间的外键关系 - - 自动添加被引用和引用当前表的关联表 +- **LLM 精筛**:`_llm_select_tables` → `select_tables` + - 构造包含候选表名和注释的提示 + - 调用 DeepSeek 模型选择最相关的表 + - 返回相关表列表和选择理由 -### 3. SQL Generator 算法 +- **经纪商维度表优先(可选)**:`_prioritize_broker_tables` + - 问题涉及对手方/经纪商时,优先纳入 `TSBBrokerContract` 与 `MCBroker` + - 避免仅选中报表视图却无 BrokerID -- **Few-shot 示例增强**:根据用户问题从示例库中选择相似的示例,增强 SQL 生成质量 - - 计算用户问题与示例库中问题的相似度 - - 选择 top-k 个最相似的高质量示例 +- **外键双向扩展**:`_expand_relations` + - 自动添加被引用的表(外键指向的表) + - 自动添加引用当前表的表(反向外键) + - 确保 JOIN 完整性 + +- **生成紧凑 Schema 字符串**:`to_compact_string` + - 根据选中的表生成紧凑的 Schema 描述 + - 包含表名、注释、字段、类型、主键、外键等信息 + +### 3. SQL Generator SQL生成模块 + +- **Few-shot 示例增强**:`FewShotSelector` + - 支持 Chroma 向量库或 JSONL 两种存储方式 + - 根据用户问题计算语义相似度 + - 选择 top-k 个最相似的高质量示例(支持评分、标签、难度过滤) - 将示例注入到提示中,指导 SQL 生成 -- **LLM 生成 SQL**:调用 DeepSeek 模型,根据选中的表结构和示例生成 SQL 语句 - - 构造包含表结构、用户问题和示例的提示 - - 指导 LLM 生成符合特定 SQL 方言的语句 - - 清理和规范化生成的 SQL + - 可配置 `FEWSHOT_USE_CHROMA=true/false` -### 4. Validator 算法 +- **LLM 生成 SQL**:`_generate_sql` → `deepseek.chat` + - 构造包含 Schema、用户问题、示例(可选)、对话上文的提示 + - 使用 `temperature=0.0, top_p=1.0` 贪心解码 + - T-SQL 特殊要求:使用方括号 `[]`、禁止反引号、`+` 拼接字符串、今日用 `CAST(GETDATE() AS DATE)` -- **程序验证**:检查 SQL 语法是否正确,表和列是否存在于 Schema 中,是否包含危险操作 - - 使用 sqlglot 进行语法检查 - - 验证表和列是否存在于 Schema 中 - - 检查是否包含危险操作(如 DROP、DELETE 等) -- **数据库试执行(探针)**:在程序验证全部通过后,于已配置的业务库上对 SQL 做只读试执行,结果用单一状态码表示,**不把具体行列数据交给 Validator LLM**: - - **1**:执行成功,且**有返回行或有返回列**(列名或数据行至少其一非空)→ **跳过** Validator 的语义审核 LLM,**直接将 SQL 交付用户** - - **0**:执行成功,但**既无列也无行** → 仍交付 SQL,**跳过** Validator 语义审核 LLM,另调 LLM 生成简短中文说明(可能原因 + 请用户补充条件),随响应一并返回 - - **-1**:执行失败 → 视为本次 SQL 不可用,**不交付**,进入重试;**不调用** Validator 语义审核 LLM - - 未配置 `database_url` 时跳过探针(可选告警),此时走 **LLM 语义验证** 作为兜底 -- **LLM 语义验证**:在未命中上述探针结果(即未配置库、未执行探针)时调用 DeepSeek,验证 SQL 语义是否符合用户意图 +- **SQL 方言归一**:`normalize_sql_for_dialect` + - 使用 sqlglot 清理和规范化生成的 SQL + - 提取 markdown 代码块中的 SQL + +### 4. Validator SQL验证模块 + +- **程序校验**(确定性规则,按顺序执行): + - **语法验证**:`validate_sql_syntax`(sqlglot) + - **Schema 一致性验证**:`validate_schema_consistency`(检查表和列是否存在于 Schema 中) + - **危险操作检查**:`check_dangerous_operations`(DROP、DELETE、UPDATE、INSERT、ALTER、TRUNCATE 等) + - **T-SQL 字面量规则检查**:`check_no_cjk_in_sql_string_literals`(禁止在字符串字面量中出现中文) + +- **数据库试执行(探针)**:`probe_sql_execution_status_ex` + - 仅在程序校验全部通过后执行 + - 需要配置 `database_url` 环境变量 + - 返回状态码: + - **1**:执行成功,且**有返回行** → **跳过 LLM 语义验证**,直接将 SQL 交付用户,可调用 `sql_probe_success_delivery_message` 生成交付说明 + - **0**:执行成功,但**数据行数为 0** → 跳过 LLM 语义验证,调用 `empty_result_user_feedback` 生成补充说明和追问引导 + - **-1**:执行失败 → 进入重试流程,不交付 SQL + - **None**:未配置 `database_url`,跳过探针 + +- **LLM 语义验证**:`validate_sql`(仅探针为 None 时调用) - 构造包含 SQL、表结构和用户问题的提示 - 让 LLM 评估 SQL 是否正确回答了用户问题 - 收集错误和警告信息 -- **结果评估**:验证通过则返回有效 SQL,失败则进入重试逻辑 - - 最多重试 max\_retry 次 - - 每次重试使用相同的表结构,但重新生成 SQL + - 如果程序校验已通过 Schema 一致性,则过滤掉 LLM 常误报的 `unknown_table` 和 `unknown_column` 错误 + +- **重试逻辑**: + - 最多重试 `max_retry` 次(默认 2) + - 每次重试: + - 合并上次失败 SQL 中引用的表(`extract_tables_from_sql`) + - 外键再扩展 + - 重新生成紧凑 Schema + - 将上次校验错误反馈给 LLM ### 5. 数据层支持 -- **Schema 管理**:管理数据库表结构和元数据 - - 从 JSON 文件加载表结构 +- **Schema 管理**:`SchemaManager` 管理数据库表结构和元数据 + - 从 JSON/G3SB 文件加载表结构 - 提供表和列的查询接口 - 分析表之间的外键关系 -- **向量数据库**:存储表和列的向量表示,用于快速检索 - - 使用 Chroma DB 存储向量 + - 生成紧凑的 Schema 描述字符串 +- **向量数据库**:`SchemaIndexer` + Chroma DB 存储和检索表向量 - 支持增量更新和查询 -- **Few-shot 示例库**:存储高质量的自然语言到 SQL 的示例 - - 从 JSONL 文件加载示例 - - 支持基于相似度的示例检索 -- **DeepSeek API**:提供大语言模型能力,用于表选择、SQL 生成和验证 - - 调用 DeepSeek 聊天模型 + - 可配置向量数据库路径 +- **Few-shot 示例库**:`FewShotSelector` 管理高质量示例 + - 支持 Chroma 向量库或 JSONL 文件两种存储方式 + - 基于语义相似度检索示例 + - 支持评分、标签、难度过滤 +- **DeepSeek API**:统一接口提供大语言模型能力 + - `deepseek.chat` 通用对话 + - `select_tables` 表选择 + - `validate_sql` SQL 验证 + - `normalize_nl_question` 问句归一 - 支持不同的模型参数配置 ## 技术栈 @@ -177,12 +270,16 @@ database_url] ## 算法特点 -1. **多智能体协作**:Schema Linker、SQL Generator、Validator 三个智能体协同工作,各负责专门任务,形成完整的处理流程 -2. **向量检索增强**:Schema Linker 使用向量数据库快速筛选相关表,提高表选择效率 -3. **Few-shot 学习**:SQL Generator 利用示例库增强 SQL 生成质量,学习最佳实践 -4. **多层验证**:程序校验 + 库上探针;探针为 1/0/-1 时不再走 Validator 语义 LLM(1 直接交付、0 附带无数据说明、-1 重试),未配置库时仍以 LLM 语义验证兜底 -5. **自动关联表扩展**:Schema Linker 通过外键关系自动扩展相关表,提高查询完整性 -6. **可配置性**:支持多种配置参数,如温度、最大重试次数、向量检索开关等,适应不同场景需求 +1. **模块化编排架构**:Text2SQLOrchestrator 统一编排表选择、SQL 生成、SQL 验证三大模块,各模块协同工作形成完整处理流程 +2. **问句归一化**:支持中英文问句归一为标准中文(默认开启),保证同一语义的查询生成一致的 SQL +3. **向量检索增强**:Schema Linker 使用 Chroma 向量数据库快速筛选相关表,提高表选择效率,可配置启用/禁用 +4. **经纪商维度表优先**:针对证券/期货业务场景,涉及对手方/经纪商的问题自动优先纳入经纪商维度表 +5. **Few-shot 学习**:SQL Generator 利用示例库增强 SQL 生成质量,支持 Chroma 向量库或 JSONL 存储方式,可配置启用/禁用 +6. **多层验证机制**:程序校验(语法 + Schema 一致性 + 危险操作 + T-SQL 字面量规则)+ 库上探针 + LLM 语义验证 +7. **智能探针分流**:探针返回 1(有数据)直接交付、0(无数据)附带说明引导用户补充条件、-1(失败)自动重试,None 时走 LLM 语义验证兜底 +8. **自动关联表扩展**:通过外键关系双向扩展相关表,确保 JOIN 完整性 +9. **对话上下文支持**:支持多轮对话的上下文合并,处理指代消解和续问 +10. **可配置性**:支持多种配置参数,如最大重试次数、向量检索开关、Few-shot 配置、问句归一开关等,适应不同场景需求 ## Few-shot 学习详细说明 @@ -192,28 +289,54 @@ Few-shot 学习是一种机器学习方法,指通过少量示例来指导模 ### 在 Text2SQL 系统中的应用 -在 Text2SQL 系统中,Few-shot 学习主要应用于 SQL Generator 智能体,具体流程如下: +在 Text2SQL 系统中,Few-shot 学习应用于 SQL 生成阶段,通过 `FewShotSelector` 组件实现,具体流程如下: -1. **示例库构建**:系统维护一个存储高质量自然语言到 SQL 示例的库,从 JSONL 文件加载。这些示例包含各种类型的 SQL 查询场景,如简单查询、复杂连接、聚合操作等。 -2. **相似度匹配**:当用户提出自然语言问题时,系统会计算该问题与示例库中问题的语义相似度。 -3. **示例选择**:基于相似度排序,选择最相关的 top-k 个高质量示例。 -4. **提示增强**:将这些示例注入到给大语言模型的提示中,指导模型生成更准确的 SQL 语句。 -5. **生成指导**:模型参考示例的结构和风格,结合用户问题和表结构,生成符合要求的 SQL 语句。 +1. **示例库构建**:系统维护一个存储高质量自然语言到 SQL 示例的库,支持两种存储方式: + - **Chroma 向量库**:`FEWSHOT_USE_CHROMA=true` 时使用,提供高效的相似度检索 + - **JSONL 文件**:传统方式,从 `all_samples.jsonl` 加载示例 + - 每条示例包含:问题(中文/英文)、SQL 语句、评分、标签、难度等级等元数据 + +2. **相似度匹配**:当用户提出自然语言问题时,使用嵌入模型将问题转换为向量,计算向量相似度 + +3. **示例选择**:基于相似度排序,支持多维度过滤: + - `top_k`:选择最相关的 top-k 个示例(默认 3) + - `min_rating`:最低评分过滤(默认 7) + - `required_tags`:必须包含的标签 + - `max_difficulty`:最大难度过滤 + +4. **提示增强**:将选中的示例注入到给大语言模型的提示中,格式为: + ``` + 参考以下相似示例的SQL编写风格: + + 示例 1: + 问题:xxx + SQL: + xxx + + 【当前Schema】 + xxx + ``` + +5. **生成指导**:模型参考示例的结构和风格,结合用户问题和表结构,生成符合要求的 SQL 语句 ### 技术实现 -- **示例存储**:使用 JSONL 格式存储示例,每条示例包含自然语言问题和对应的 SQL 语句。 -- **相似度计算**:利用预训练的嵌入模型将问题转换为向量,计算向量相似度。 -- **示例选择**:根据相似度得分选择最相关的示例。 -- **提示构造**:将选中的示例与用户问题、表结构一起构造提示,确保模型能理解任务要求。 +- **示例存储**:支持 Chroma 向量库持久化或 JSONL 文件加载两种方式 +- **向量索引**: + - Chroma 模式:使用 Chroma DB 存储和检索向量 + - JSONL 模式:内存 numpy 数组 + 可选 .npy 缓存 +- **相似度计算**:利用预训练的嵌入模型将问题转换为向量,使用余弦相似度计算 +- **示例选择**:根据相似度得分和元数据过滤选择最相关的示例 +- **提示构造**:将选中的示例与用户问题、表结构一起构造提示,确保模型能理解任务要求 ### 优势 -1. **减少数据需求**:不需要大量的标注数据,仅需少量高质量示例。 -2. **提高生成质量**:通过示例指导,模型能生成更符合特定场景的 SQL 语句。 -3. **学习最佳实践**:示例库可以包含领域专家编写的高质量 SQL,使模型学习到最佳实践。 -4. **适应不同场景**:通过扩展示例库,可以适应不同领域和复杂度的 SQL 生成需求。 -5. **灵活性**:可以根据具体应用场景调整示例库,提高系统的适应性。 +1. **减少数据需求**:不需要大量的标注数据,仅需少量高质量示例 +2. **提高生成质量**:通过示例指导,模型能生成更符合特定场景的 SQL 语句 +3. **学习最佳实践**:示例库可以包含领域专家编写的高质量 SQL,使模型学习到最佳实践 +4. **适应不同场景**:通过扩展示例库,可以适应不同领域和复杂度的 SQL 生成需求 +5. **灵活性**:支持 Chroma 和 JSONL 两种存储方式,可根据场景选择 +6. **高质量保障**:通过评分、标签、难度等元数据过滤,确保选用高质量示例 ### 应用效果 @@ -223,20 +346,29 @@ Few-shot 学习是一种机器学习方法,指通过少量示例来指导模 - 生成更符合用户意图的 SQL 语句 - 减少生成错误 - 提高系统的泛化能力 +- 通过示例学习特定业务场景的 SQL 编写风格 ## 性能优化 -1. **向量索引预构建**:提前构建向量索引,加速首次查询 -2. **缓存机制**:缓存常见查询的结果,提高响应速度 -3. **并行处理**:对多个查询进行并行处理,提高系统吞吐量 -4. **模型调优**:调整 LLM 参数,平衡生成质量和速度 +1. **向量索引预构建**:`build_vector_index` 方法提前构建向量索引,加速首次查询 +2. **懒加载机制**:向量索引在首次使用时才加载,避免启动开销 +3. **Few-shot 缓存**:向量缓存到 `.npy` 文件,换模型自动重建 +4. **贪心解码**:SQL 生成使用 `temperature=0.0` 贪心解码,提高一致性和速度 +5. **库探针优化**:仅取 1 行数据(`max_rows=1`)判断探针状态,最小化数据库负载 +6. **SQL 执行优化**: + - 聚合查询优先直接执行而非 COUNT 包裹 + - 自动去掉派生表内无意义的 ORDER BY + - 支持多种 SQL 方言的语法处理 ## 扩展性 -1. **支持多种数据库**:通过配置支持不同的 SQL 方言 -2. **可插拔的 LLM**:支持替换不同的大语言模型 -3. **自定义示例库**:可根据特定领域扩展示例库 +1. **支持多种数据库**:通过 sqlglot 支持不同的 SQL 方言(T-SQL、MySQL、PostgreSQL 等) +2. **可插拔的 LLM**:统一通过 `DeepSeekClient` 接口调用,支持替换不同的大语言模型 +3. **可配置的嵌入模型**:通过环境变量支持 OpenAI、ModelScope、DashScope 等多种嵌入 API +4. **自定义示例库**:可根据特定领域扩展示例库,支持 Chroma 或 JSONL 格式 +5. **Schema 灵活加载**:支持从 JSON 文件或 G3SB 格式加载 Schema +6. **意图分类可扩展**:支持 rules 和 hybrid 两种模式,可扩展更多分类策略 ## 总结 -Text2SQL 算法通过多智能体协作、向量检索和大语言模型技术,实现了从自然语言到 SQL 的高效准确转换。算法流程清晰,逻辑完善,具有良好的可扩展性和可配置性,能够满足不同场景下的 SQL 生成需求。 +Text2SQL 算法通过模块化编排架构、向量检索、问句归一化和 Few-shot 学习技术,实现了从自然语言到 SQL 的高效准确转换。系统采用多层验证机制(程序校验 + 库上探针 + LLM 语义验证),配合智能探针分流策略,确保生成的 SQL 既正确又符合用户意图。算法流程清晰,逻辑完善,具有良好的可扩展性和可配置性,能够满足证券/期货等金融业务场景下的 SQL 生成需求。