From a55fd18915197da01e745ead8fc04c9133bf1b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E8=BE=85=E5=85=83?= <2401926342@qq.com> Date: Wed, 15 Apr 2026 11:25:19 +0800 Subject: [PATCH] Enhance impact analysis and configuration for vector persistence and API integration. Update `main.spec` to clarify data handling for ChromaDB, ensuring vector directories are excluded from packaging. Modify `Text2SQLOrchestrator` to unify question normalization for consistent SQL generation across languages. Introduce `SchemaIndexer` improvements for persistent vector storage and optimize embedding retrieval processes. Update documentation and comments for clarity on configuration changes and behavior adjustments. --- IMPACT_ANALYSIS.md | 162 ++++++++++++++++++ backend/__pycache__/main.cpython-312.pyc | Bin 17525 -> 17563 bytes .../__pycache__/orchestrator.cpython-312.pyc | Bin 31530 -> 31412 bytes backend/agents/orchestrator.py | 25 ++- .../__pycache__/prompts.cpython-312.pyc | Bin 20571 -> 22211 bytes backend/config/prompts.py | 18 ++ .../deepseek_client.cpython-312.pyc | Bin 15026 -> 15475 bytes backend/llm/deepseek_client.py | 21 ++- backend/main.py | 2 +- .../__pycache__/indexer.cpython-312.pyc | Bin 10804 -> 13499 bytes backend/schema/indexer.py | 85 +++++++-- .../dialog_classifier.cpython-312.pyc | Bin 10777 -> 10777 bytes .../fewshot_chroma_store.cpython-312.pyc | Bin 10693 -> 11961 bytes backend/utils/fewshot_chroma_store.py | 30 +++- main.spec | 25 ++- 15 files changed, 318 insertions(+), 50 deletions(-) diff --git a/IMPACT_ANALYSIS.md b/IMPACT_ANALYSIS.md index 8d35455..7a04970 100644 --- a/IMPACT_ANALYSIS.md +++ b/IMPACT_ANALYSIS.md @@ -206,3 +206,165 @@ |-------------|------| | `USE_LOCAL_EMBEDDING`、`EMBEDDING_MODEL_PATH` | 代码不再读取;`.env` 中可删去以免误解。 | | CLI `--embedding-model` | 已删除。 | + +--- + +# Impact Analysis Report — main.spec:向量持久化目录不落包(追加) + +## 1. 改动概览 + +- **背景与目标**:在 PyInstaller 配置中明确约定「业务侧 Chroma/向量持久化目录」不加入 `datas`,避免误将 `data/embeddings` 等打入 onefile;实际打包行为与此前一致(`datas` 中本未包含上述目录)。 +- **涉及模块**:`main.spec`。 +- **改动类型**:配置说明 / 文档化。 + +## 2. 方法级改动 + +| 位置 | 变更 | +|------|------| +| `main.spec` 顶部注释与 `datas` | 写明 `VECTOR_DB_PATH`、`FEWSHOT_CHROMA_PATH` 对应目录外置;区分 chromadb **wheel 依赖**与**用户向量库文件**;`EXE` 处注释澄清 `a.datas` 含义。 | + +## 3. 调用方与影响范围 + +- **调用方**:仅 `pyinstaller main.spec` 打包流程;运行时仍依赖环境变量或旁路目录提供向量库(与代码逻辑无变更)。 +- **破坏性变更**:否。 + +## 4. 风险与回滚 + +- **风险级别**:低。 +- **回滚**:还原 `main.spec` 对应注释块。 + +**回滚方式是否简单**:是。 + +## 5. 验证与测试 + +- 打包前确认 `datas` 无 `data/embeddings` 等元组;打包后 exe 旁按需放置向量目录并配置 `.env`。 + +--- + +# Impact Analysis Report — 前端默认 NL API 地址(追加) + +## 1. 改动概览 + +- **目标**:前端开发与 UAT 打包默认后端 origin 改为 `http://0.0.0.0:8041`;同 host 解析时默认端口改为 `8041`。 +- **涉及模块**:`ai-g3sb-backman-web/vite.config.ts`(`VITE_DEV_API_TARGET` 默认值)、`ai-g3sb-backman-web/src/api/packagedOrigin.ts`、`ai-g3sb-backman-web/src/api/config.ts`;`src/locales/{zh,tw,en}.ts` 中管理端 API 占位示例同步。 +- **改动类型**:配置默认值变更。 + +## 2. 调用方与破坏性变更 + +- **调用方**:Vite 开发代理 `/g3sb`;非 test 构建注入的 `window.__BACKMAN_API_ORIGIN__` 与 `apiBase()` 默认打包 origin。 +- **破坏性变更**:否(仍可通过环境变量 `VITE_DEV_API_TARGET`、`VITE_PACKAGED_API_ORIGIN`、`VITE_PACKAGED_API_PORT` 覆盖)。 + +## 3. 风险与回滚 + +- **风险级别**:低。说明:浏览器直连 `0.0.0.0` 在部分环境下可能不如 `127.0.0.1` 可靠;若遇连接问题可改用 `VITE_*` 覆盖为实际可访问地址。 +- **回滚**:还原上述三处默认值或设置环境变量覆盖。 + +**回滚方式是否简单**:是。 + +## 4. 配置变更 + +| 项 | 说明 | +|----|------| +| `VITE_DEV_API_TARGET`(可选) | 未设置时默认 `http://0.0.0.0:8041`。 | +| `PACKAGED_API_ORIGIN` / `VITE_PACKAGED_API_ORIGIN` | 代码内默认改为 `http://0.0.0.0:8041`。 | +| `VITE_PACKAGED_API_PORT`(可选) | `same-host`/`auto` 时未设置则默认 `8041`。 | + +--- + +# Impact Analysis Report — SchemaIndexer 持久化 Chroma(追加) + +## 1. 改动概览 + +- **背景与目标**:原先 `SchemaIndexer` 固定使用 `EphemeralClient`,进程内集合恒为空,每次请求粗筛都会全量计算约 2500+ 张表的 embedding;用户已在 `data/embeddings/chroma` 等目录落盘向量库,应直接复用。 +- **涉及模块**:`backend/schema/indexer.py`、`backend/agents/orchestrator.py`(日志与 `force_rebuild`)、`backend/utils/fewshot_chroma_store.py`(注释)、`main.spec`(打包说明注释)。 +- **改动类型**:缺陷修复 / 行为优化(性能与成本)。 + +## 2. 方法级改动 + +| 位置 | 变更 | +|------|------| +| `SchemaIndexer.__init__` | 默认 `chromadb.PersistentClient(path=persist_dir)`,创建目录;`SCHEMA_INDEXER_EPHEMERAL=true` 或 `use_ephemeral=True` 时仍为 `EphemeralClient`。默认 `persist_dir` 与 `VECTOR_DB_PATH` 对齐为 `./data/embeddings/chroma`。 | +| `SchemaIndexer.get_statistics` | `chroma_mode` 区分 `persistent` / `memory`。 | +| `Text2SQLOrchestrator._coarse_filter` | 每次粗筛前调用 `SchemaIndexer.ensure_index_for_schema`:索引条数与当前 Schema 表数一致则跳过向量化;空库则构建;不一致则 `force_rebuild` 重建。 | +| `SchemaIndexer.ensure_index_for_schema`(新) | 对比 `collection.count()` 与 `len(schema_manager.get_tables())`,避免无意义重复向量化,并在 Schema 增删表后自动重载索引。 | + +## 3. 调用方与影响范围 + +- **调用方**:`Text2SQLOrchestrator._get_vector_index()` 传入的 `persist_dir` 仍为 `vector_db_path` / 环境变量 `VECTOR_DB_PATH`;向量粗筛路径经 `ensure_index_for_schema`。 +- **破坏性变更**:否。若依赖「每次进程冷启动强制空向量库」的测试,需设置 `SCHEMA_INDEXER_EPHEMERAL=true`。 + +## 4. 风险与回滚 + +- **风险级别**:低。说明:Embedding 模型或维度变更时,旧持久化向量与当前 encoder 不一致可能导致检索质量下降,需删除目录或调用带 `force_rebuild` 的构建逻辑重建。 +- **回滚**:还原 `indexer.py` 相关提交或设置 `SCHEMA_INDEXER_EPHEMERAL=true`。 + +**回滚方式是否简单**:是。 + +## 5. 配置变更 + +| 项 | 说明 | +|----|------| +| `VECTOR_DB_PATH` | 既有;默认 `./data/embeddings/chroma`,现为 Schema Chroma 真实持久化根路径。 | +| `SCHEMA_INDEXER_EPHEMERAL`(可选) | 设为 `true`/`1`/`yes` 时使用内存 Chroma(与旧行为一致)。 | + +--- + +# Impact Analysis Report — 中英文问句归一以提升 SQL 一致性(追加) + +## 1. 改动概览 + +- **背景与目标**:同一业务语义分别用中文、英文提问时,原先仅对「纯英文」问句做英译中,中文问句原样进入检索/生成,导致与英文路径的表述不一致,向量粗筛与 SQL 可能分叉。 +- **涉及模块**:`backend/config/prompts.py`(归一提示词 + SQL_GENERATOR 约束)、`backend/llm/deepseek_client.py`、`backend/agents/orchestrator.py`、`backend/main.py`(CLI 帮助文案)。 +- **改动类型**:行为优化;默认仍受 `TRANSLATE_EN_TO_ZH` / `--no-translate-en` 控制。 + +## 2. 方法级改动 + +| 位置 | 变更 | +|------|------| +| `DeepSeekClient.normalize_nl_question_for_text2sql` | 新增:`temperature=0` 下将中/英问句归一为一句中文。 | +| `DeepSeekClient.translate_nl_question_to_zh` | 改为委托上述方法(兼容旧名)。 | +| `Text2SQLOrchestrator.generate` | 当 `translate_english_to_zh` 为 True 时,对**所有**非空问句调用归一(不再仅限 `looks_like_english_only`)。 | +| `SQL_GENERATOR_SYSTEM` | 新增硬性约束第 6 条:中英文同义时 SQL 逻辑须一致。 | + +## 3. 调用方与破坏性变更 + +- **调用方**:所有经 `generate()` 的 Text2SQL 请求(含 `api_server` 默认 `TRANSLATE_EN_TO_ZH` 开启时)。 +- **破坏性变更**:否。关闭归一:`TRANSLATE_EN_TO_ZH=false` 或 `--no-translate-en` / `Args.no_translate_en`,行为与改动前「中文不归一、英文仍可不译」一致。 + +## 4. 风险与回滚 + +- **风险级别**:低~中。每次成功归一增加一次 LLM 调用(延迟与 token);归一失败时回退原文(与旧失败策略一致)。 +- **回滚**:关闭环境变量或还原相关文件。 + +**回滚方式是否简单**:是。 + +## 5. 配置变更 + +| 项 | 说明 | +|----|------| +| `TRANSLATE_EN_TO_ZH` | 语义扩展为「是否做问句归一中文」;`false` 关闭额外 LLM 归一步骤。 | + +--- + +# Impact Analysis Report — Few-shot Chroma 默认读盘(追加) + +## 1. 改动概览 + +- **问题**:`FEWSHOT_USE_CHROMA=true` 时 `FewShotChromaStore` 仍默认 `EphemeralClient`,进程内 `count` 恒为 0,每次启动从 JSONL 重算 embedding,忽略已灌库的 `data/embeddings/chroma_fewshot`。 +- **模块**:`backend/utils/fewshot_chroma_store.py`。 +- **类型**:缺陷修复。 + +## 2. 行为变更 + +| 项 | 说明 | +|----|------| +| 默认 | `persist_to_disk=None` 时默认使用 `PersistentClient`(`FEWSHOT_CHROMA_PATH` 或 `./data/embeddings/chroma_fewshot`)。 | +| 退出 | `FEWSHOT_CHROMA_EPHEMERAL=true` 或构造参数 `persist_to_disk=False` 时仍为内存 Chroma。 | + +## 3. 破坏性变更 + +- **否**。灌库脚本显式 `persist_to_disk=True` 不变。 + +## 4. 配置 + +| `FEWSHOT_CHROMA_EPHEMERAL` | 可选;`true` 时与旧版「仅内存」行为一致。 | diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 9481539449ed982a99c4e7c3df95c8432535600c..7efc9fb6c0ec4c4d4a4ace12513e7371726fa73e 100644 GIT binary patch delta 146 zcmey`!8p5vlt7rWmFwd@M-Jjm($n2oWAa9|I(*> zCq3EG@MOo@XVco>@9B8Cd&Y}(OP=m&csgPI`#rOO;xD>4KAqeCw6z1G5~u~D_{r|2 rPdipB1PA&k01bH2zGZWOZ4o1*?dHRF7EEj(m>Br{m^S}&C}#!$Y;jDr delta 91 zcmV-h0Hpt$i2?P90S(Iy4GI7N000;2-f2~_4G07QFS8Z|134mF0002xg|q3Gt>}xf x=9Y))m#*cHrRayM<%+22uaV@qx|1(G7qfLeZ~*~2v*tcH0t46$YO^FkcLQ-8CO-fG diff --git a/backend/agents/__pycache__/orchestrator.cpython-312.pyc b/backend/agents/__pycache__/orchestrator.cpython-312.pyc index 852fe4e1f094a5a6103eb1b7921a445409278b77..8b012798a7990b3c9a381a77401f2e412e733dc8 100644 GIT binary patch delta 4327 zcmai1dsI}%8K1eAeLrCzunW8FvMg^gzLMsFf)TW-At0C+%JR4u1Xj?yix4|k&^(Zo z`pBd)i6IhWj0$RWJ&lQ))I84dBquqdNl7?Klcs=h+SD~B_MD_?J9C#s#P)Rm`0nrf z=9}*^^Uch+hpxdBKS2G5I-MGjU(LV&)G#C-(_dqitE%hy=3rn|NE>rm!Q94%e%5>m zJ4Q~v=_(gmdDqyk-(Nnxb>h^s<45*gKHWWf^o^{nvF^^X7rL{uCVF4IbnwlIT}MW@ zzcRY{`H_>IV*@X_mM_28HG1YL!jXwVe(gG5s1CLYeBLc&{^ip>YS)b4_*;0i>rJ9h z%#8JQjdndT(sARA6S`69ae&0 z!qrfcglgjsGq5uhYjHDhC!QDIz&yj?7vqoPz62}&a+U_apWxIp2y!eJV+XqdC+o!> z2`LJQ7*0N*z-w&Du}T|>W+&65iUFcrlm!`_U*Yhokep0XOOrG~6H$?VNZwCkq(5Rz zi-&EFM011(AQcF8q9Vvd`Nnv>*=@?!AqCOZ_p+Q}z}P=I3IP|(v3{+nyg|RzWr9i^ zt}tVlNr^k{1vXYxY*Lf~PWraNHqg#K1lnL`Txi%1A-*T+Z|3q$IIQH`YkdvPbu9vQ zkg3N@9O()1d~<-*B9U`^yN`_d0<~-Swbgi&qZO{l-#dOK?AW(xAo*?^cqd7Fw>jpWjBJA6 zHOnZqsS1_syD0^Ug)IAC9wf~Btc*}$!*SOmtb7CUCA8xXPl~I9z+ru`y2j7@{0)Ji zFAx-ll>#4F*U}u|1reY1RKRl~N2-s3|G;9}Iru#;O3#O4s4IO7YuQOHWl^$;8e#){ zQ0T{Pi(h3$A*_T%uU(8t+{vU$I|QCCZrfp4g9_BHX31cp@tU+0s;5T2oWMU=GQ zq&Ybzx-!FZ5>l%_C_GN(PvD9I{A_pvhx4t;2dK(HN)Ay{LP=yAMMto=U_IQ5cNJ8?ywKHxZ#D2B{`0*{;cKDH zVkeCMBUPeXDp(20WCSC{$3m6&)j)VGbhcy#qd7xG=P4P)&N9#9ixhrB$peH8>$#d- zu4YYZjW5{J;tvR)(veO|7SdtuhH8HUR~_Vif%SgDP34c`N6Tmuhsy4hN5aQ{Ez6Tn zy2vg6g*@T{`^)FTPw}4eW%409??e1;c{ki2YFqg!E6-N}unXWUQ@Tpt07M0ni%L8@ z*QyYnL~>!P3jb?;JYK)dqM15TjSz=b_99V*RKn6AnI8EeRrJVlOw%gj=60kKl}L^h z9D5g0K`Me`C<;o~J)PC4qem}KhE$xKQ`o_OwWerVLaZ^IvKex4Bbjyma5h_lYW#Dq zBX-I+1V|}DB*Qr-FQY+tkIfQ`G~{N{^t03>0HRisCP`B5lvFn}s(CsxOU~(~tHsdE zMzbSVf>G3Sa?wx^L?ijea!`yT*o5>?&9|DHNEz&4gr{fP^Tr}Q7f0jY4SJdD)`-;D zltD7=LV#RFgF#!Swpaf(<%s^5!gC$_Qt(K_=wan^dl8Y@GFHEN^;ITz53k;A)OXupBMU+Jca)<_^w}3`|rvp()IJ%{ zmadK4r+hm&Gue3;B1bNAq^1rL(_tmogdBMWa{85%Ynh5-1Or*Y)W$oa7I3#)W0@s+ zO)Z;#A?4BigSq4&caWl<{eN@!qF9o9Eb>m~{=ahY-jsu3a@Qeu3o_D7kau8CzdRCQ z8St1ukUk>PkF+dqQ61UL4mDComkxr5Xc?+EsdunFT@iG>`KTqwqLvYDZNPJ*tz=Qn zxr6hf)``8TW~rB{hoVthR;fIu*)NOOcV2fe!)#hK#06Q?L!`SA+2JW;u*W?!*vz@1 zLp5JPe2`mo`(*e2`teB19&K`;_Xdh5+DX){^eX?RbARhCih93zCcq)hJ;g;F!*IY9rof6`Iq1Yytb|g{u-aCD@vvJbFtthlsw`+RI`|n z0DanBE+L%?wqa-e0=N&C*U$1y^$M-DgC<>lR z(M`z3Vax}X0#M3=kK$dWx!`=Ps^m`gyxm%ID|`M{8Nq|Bsx+G&)S64v*ugXz#kr~# z4tDUi#1#qbg#;PF!#dKr*781quW4=YbD_iQ7s1?2DA>pmBjr#_Q`bmCk$zw5kI8_v zH&jUoU}NB3cr8>J@RHsUKNfrd2JjcbWSETq8O(rk>}~B_bdA_4FE19}Am0fEWr&X- z$u|>DkQJk@UL&94;z=OnQ7$t^k_tcKkydW8R8RE6i4@fiilml3QeKpm3h6tF43v?hKnGXa>pYeSzoJhAk3n1qq4acnKx|hbwH8pj`t}(yk^- zcnz*;EAnQ_7JMCN3CqZrL}N6C6%;9fTEDfn+jdn&xysN-ZBN5{@>vo()un(a6R41& z5+pjqDhaBACq1l@pcdHNeWvGhVV%V4f!co6K;e}oq5ek<5YED3aB F{|8Napho}z delta 4247 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 diff --git a/backend/agents/orchestrator.py b/backend/agents/orchestrator.py index 9454b9d..79b548b 100644 --- a/backend/agents/orchestrator.py +++ b/backend/agents/orchestrator.py @@ -66,7 +66,9 @@ class Text2SQLOrchestrator: vector_db_path: 向量数据库路径 max_retry: 最大重试次数(包含首次生成) use_vector_search: 是否使用向量检索粗筛 - translate_english_to_zh: 无中日韩字符的英文问句是否先译为中文再走检索与生成 + translate_english_to_zh: 为 True 时(默认)对**所有**非空问句做一次 LLM 归一(temperature=0), + 输出一句标准中文供检索与生成;使同一语义的中英文表述对齐,从而 SQL 一致。为 False 时 + 不做归一(原样英文/中文)。环境变量 ``TRANSLATE_EN_TO_ZH=false`` 可关闭。 """ self.schema_manager = schema_manager self.max_retry = max_retry @@ -120,7 +122,7 @@ class Text2SQLOrchestrator: f"[OK] Text2SQLOrchestrator初始化完成: " f"max_retry={max_retry}, use_vector_search={use_vector_search}" + (f", fewshot=on" if self.fewshot_enabled else "") - + (", en→zh=on" if self.translate_english_to_zh else ", en→zh=off") + + (", nl→zh_norm=on" if self.translate_english_to_zh else ", nl→zh_norm=off") ) @staticmethod @@ -174,12 +176,7 @@ class Text2SQLOrchestrator: try: indexer = self._get_vector_index() - - # 确保索引已构建 - if indexer.count() == 0: - logger.info("向量索引为空,正在构建...") - indexer.build_index(self.schema_manager, force_rebuild=True) - logger.info(f"[OK] 索引构建完成,共 {indexer.count()} 张表") + indexer.ensure_index_for_schema(self.schema_manager) # 检索 logger.info(f"[Orchestrator] 开始向量检索: query='{question[:50]}...'") @@ -585,26 +582,24 @@ class Text2SQLOrchestrator: Returns: GenerationResult对象 """ - from utils.question_locale import looks_like_english_only - original_question = (question or "").strip() translation_meta: Dict = {} work_question = original_question - if self.translate_english_to_zh and looks_like_english_only(original_question): + if self.translate_english_to_zh and original_question: try: - zh = self.deepseek.translate_nl_question_to_zh(original_question).strip() + zh = self.deepseek.normalize_nl_question_for_text2sql(original_question).strip() if zh and len(zh) >= 2: work_question = zh translation_meta["question_original"] = original_question translation_meta["question_zh_normalized"] = zh logger.info( - "[GEN] 英文已译为中文:%s", + "[GEN] 问句已归一中文:%s", zh[:120] + ("…" if len(zh) > 120 else ""), ) else: - logger.warning("[GEN] 英译中结果为空或过短,使用原文") + logger.warning("[GEN] 归一结果为空或过短,使用原文") except Exception as e: - logger.warning("[GEN] 英译中失败,使用原文: %s", e) + logger.warning("[GEN] 问句归一失败,使用原文: %s", e) question = work_question dc_raw = (dialog_context or "").strip() diff --git a/backend/config/__pycache__/prompts.cpython-312.pyc b/backend/config/__pycache__/prompts.cpython-312.pyc index ae331e7685c7d7bde6c563e5629c647ef52131ce..df7b127bb42aa343d294b781ec7d54915fa5c571 100644 GIT binary patch delta 1568 zcmaKs+iN3r6vvxYk%sQR*j>BIx>HKo-C49+FCh4^wRD?8FSxC{YhQ}E3o7h`tHKIG zCfQsjX)=@Sq)j*JB~48?bT>(BVl&O;^5BC)5$dagpq=xZ-+<8i7wGxTwBU;d5;DW@ zch2{G&i9;OGmh^Ij#IlQPdeN1GxGOupLlP?Pjxx&#V>r;aqIc%&Rbp6r@uOT>4mMu z!S)kBUM~OMzWbc}tyegn*VO`@-6Hcrh(+|Bkgf_eSmF7Dddf)TiC8t0MKikqa*5t= zK=oTAv8AhFqN>n%fM<2Jh5_*QzTgFUgRXmYH3i{S5Cdd6Mtm})n?_p2*avlyBr~*F z18D{PA_jNcH;FiJ3!tlc+S)v*hYsoyy^%2zD@2SLDIYia-Y5r)d8SpvLw(>cQ%{0c zOVIS~3m&Mgn?YGuCF)5*OvE=gTtnA}IDEso2Ck2c4|8N`RoAvzfUvo$>8&La@#y1L zshEuCjdc$xWO*La8KV%=wJMP|XmgoKD;11KQK%N7?xjM6M6;+IOkNbe(mRxlIx+>Y4&Ig`2v4?~8s`1J<%HL=Y&*IUC=;5GDClSV|&3 zr<0a6@aJsoc$Nk?4Bs{ipm_o5VGEG9)iC7E^H?X(+vIp2l^!*+T7`i7p-quC6$s)L zNZ?iMm4Jzl1N=L>-D8HmnTA*#X_7U;sAMsPDTEL?LuplD^+a#9P^2XpT7Cp-dcK3A zh?a4_#w5Vy~kj7+or|=9( zZkx#pjogQHg+0z~f>1XXMEtcP1PsmBN2CW(llwr@%wW`Z+d)08yRCFI7%&C25pywF z+#qrc1a*KE)znatvhc%YCZrRcF^ zBeWE^CLsj@Kd5RuCbgp}e_egYePn4>vfpu&AANlL&W9W+=gnZ46=NYs-cX;zo7izE z?U;=illc14jH0stkcdMGvB!33Y#|N`(k6)=kQXo#_Q8!cLnw;%>*_38Wv*hx0+w9K zc=CknXVZb@VGZV*L^5^s39bMJswj{O7JrNdr?SPi1hWzwa&u82TVCc=3!xDb(I@tC z{BLM7PBCJfL=QqtLK|*2>^))%F$8Gu1w?8-UciPBDMwpzhCiZf63!ZqDJ>x!$sV0BVz{(%Adn8zIrtDY82~aA>L$Q z*~+qJ#GVaXyFH-F6;~&R*jWC$fGX*uHLG3TFR$Q^wQZ#cuJaxCD6?}ZUwOs#@-vPz z<)6l;9q;xG4vbHXj|>isj!X~DjE~M-owznLHFR@|ZG)M~4<@IE#(wGkze7s*#9b$c z=^Xg|{0C)eqPP9i6J1a5wRIleRCXu6?rpy?=GbfNIXv@J@5Q~g?!yje&-*9#+Rh(# PIlE8)+;;BnOL+4id7iQz delta 123 zcmX@SmhtuiM!wU$yj%=GAo%)j=7oTXd=iWW6V(e@D#bM=HZF1IWUA2G{F3vu@Macy zX6?=U{A5`fMK<3Fj$&k#pS&Pca`KGOATBwec19pB-ZuG5s48bBlLX5J29Xb8la<11 ZM7WegnJzHMe~{o(6urnGTO(9{(_V>Ygs0{&d2`m+k$}7B@WKvgpZ< zK84^wABE>r=0EG0@M8P=r~6taE2@e(DLXs*`TKb~JNkG=xyJkX#Jl+i#fP{?gct#p z#|K9Shq(IQQpBfdvW)JDWTm|PqTIxs%&OG*yqx&L($wOT%>2CgwEUv@lGKV4qvFDx zDnpml)PmsD)NJRR%+$ORJ;GW%7#SFfmjQ#cf#EyXaNrqJdVRt7eJfFP- z7!Sa>e6pkWXw9;k{G8>jLr{~GO{ZDrFJ=p<7AQ6S&pV&q zd6X{msB|#iT?Qs4A zqBgHL;$&h>*}T)Ffr&9^vX%K?u0EiXia7>e5_Kei6xtz==;{m1|!zgKWhcC*n109K^Ds{jB1 diff --git a/backend/llm/deepseek_client.py b/backend/llm/deepseek_client.py index 555a79b..920b700 100644 --- a/backend/llm/deepseek_client.py +++ b/backend/llm/deepseek_client.py @@ -325,25 +325,34 @@ class DeepSeekClient: kwargs.setdefault("top_p", 1.0) return self.chat_with_json(messages, **kwargs) - def translate_nl_question_to_zh(self, question: str) -> str: + def normalize_nl_question_for_text2sql(self, question: str) -> str: """ - 将主要为英文的自然语言分析问题译为中文,便于与中文 Schema 注释 / 向量索引对齐。 + 将中文/英文问句归一为**一句**标准中文(temperature=0),使同一语义的中英表述 + 走同一套向量检索与 SQL 生成路径。 """ - from config.prompts import TRANSLATE_NL_TO_ZH_SYSTEM, TRANSLATE_NL_TO_ZH_USER + from config.prompts import ( + CANONICALIZE_NL_FOR_TEXT2SQL_SYSTEM, + CANONICALIZE_NL_FOR_TEXT2SQL_USER, + ) q = (question or "").strip() if not q: return "" messages = [ - {"role": "system", "content": TRANSLATE_NL_TO_ZH_SYSTEM}, - {"role": "user", "content": TRANSLATE_NL_TO_ZH_USER.format(question=q)}, + {"role": "system", "content": CANONICALIZE_NL_FOR_TEXT2SQL_SYSTEM}, + {"role": "user", "content": CANONICALIZE_NL_FOR_TEXT2SQL_USER.format(question=q)}, ] msg = self.chat(messages, temperature=0.0, top_p=1.0, max_tokens=512) text = (msg.content or "").strip() - # 只取首行,避免模型附加说明 line = text.splitlines()[0].strip() if text else "" return line.strip("「」\"'“”") + def translate_nl_question_to_zh(self, question: str) -> str: + """ + 兼容旧名:与 :meth:`normalize_nl_question_for_text2sql` 相同(不再仅限英文)。 + """ + return self.normalize_nl_question_for_text2sql(question) + class AsyncDeepSeekClient: """ diff --git a/backend/main.py b/backend/main.py index 5a8561e..386f674 100644 --- a/backend/main.py +++ b/backend/main.py @@ -377,7 +377,7 @@ def main(): parser.add_argument( "--no-translate-en", action="store_true", - help="关闭英文问句自动译为中文(默认开启;也可用 TRANSLATE_EN_TO_ZH=false)", + help="关闭问句归一中文(默认开启:中英均先归一句中文以利 SQL 一致;也可用 TRANSLATE_EN_TO_ZH=false)", ) args = parser.parse_args() diff --git a/backend/schema/__pycache__/indexer.cpython-312.pyc b/backend/schema/__pycache__/indexer.cpython-312.pyc index 916de9c76d869057c976559b294b44e8c2f0c123..90af973db06aa2a4871ff069ff0865b843643136 100644 GIT binary patch delta 5310 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 delta 2712 zcmY*bdu&tJ89(RxUi)4r4l%JECnkvlgya&E00CNHOX$jYY!$7s3A{ZZ!pj+#$Hp&)_vi<{Rn3zElc;*I05dzDyZfLPR0Y&t;U>9dur z@A#Ikx@wASza7ff4AfAf5Ec|Okw}u&zE9B)Ap>EBY$jIVFtPOam503f$3NcCt#4

*-IAx|w_~Wt-GfpGmoU#8TRPK*k0>?)?$qRl*2F zLk1L!4)_dW5G!seOtpMWvs9)H`z`ITHsF_%2FuR^R)Fc2&J4?7#;|S$K^m0OMym>J zszDlJRUoZqp$0esbZS7S#+nBlt$`dMQCWTN`Wty%oYj78rdZz&aLl#B(q@7x%-$`AgX<48f{k+q;27)>SaBCMX#RL zs5;*mtuV%y3_sJC;PWw>f3QC zvh(lyF7cphVJtM9=V>;`*-#;4Tb`dYx4?4~AB6v)#iE#Y451#tQ(d;t<#BPeehW3l zmHJ-VC931SPqv~^OXmwY*YiO)Jl_a&`9ic?gd(5Ka=Znpu*GcVvcb9T$g1+BI2d0` ztHp(QqINObbs(4sOGPohxosto4&D}6`|{##u=T2B*n=5-*~Hd{1YJMD8g5Y9KCvlr zR;4S%7cGn0yU-EdRi0tN1f_Cm=I~X>>;<@sH=C*P9Ht}3*D;=840&K417Z+CLk(sc_893uJO>yI?W78vzci zQol^tRCy2Vr2Fzg4dTCxm+X{TJ%$n)vGMhNX(lgd*)}ime2VAbDmVS*xCc@5+X!(0 z91^EsyB=i@UPtASAy&7opZ;dzeA{J}%KOGxs)gz-TzOgj% ztPDi<4F{30MZnjeClO|6^*t)XbSQ8kKtLF5o2B^vx-Z@8-IDnaA}%a-XuIg{+?SX; zAq!@Yc;KtWC!KNS1`(fiHYG97G`g>>*$aIga9`5IWO$W$YT5tRSo)jlc48U@Ecu~bXHEWNqGu%JeuX5;@z%S^yRRF&Oavlx);_yCq+M) zE7oZD6UygAOmx3jhcf;=fPNB;V7oJRss+Pp4w4Vy36-0FI?;lzS;lCMDW#NAiFXYIt@SvL- zvYBHlcaxyI37!#nj`Yek)o(_{)Qa8%GVUcH;0A(BNFB=OZN3TjyAU)444QuzVHjZ# z!d`?Ng3R_1Qey~mq;ZBgeitRw0^vLWI80`gfTow?WI@jrv!#2+XwjafH|9r68WJTx zi8Yi0lGI6K(>*CJZ4qCr{B2WA3!YN%6L^%5K<{Q9KZ1uOUS1XTV>5ZPxZc|m@d9>! zWCSXLpHX*@of+b~NUT~s_9E)YxtGCxIbcu!N;Yp5Y}Ut*fgX;l(+^PcQA%eNg{t>- zLTj&)HP^|qDYEP{Qge;Oi=^X*(RieO+Gw0K8ee;S%2<5R|1ZDoqrra?L>cb?0a28v A*#H0l diff --git a/backend/schema/indexer.py b/backend/schema/indexer.py index 6121750..21d34b6 100644 --- a/backend/schema/indexer.py +++ b/backend/schema/indexer.py @@ -2,11 +2,13 @@ Schema向量索引构建器 - 基于ChromaDB """ +import logging +import os +from pathlib import Path +from typing import Any, Dict, List, Optional + import chromadb from chromadb.config import Settings as ChromaSettings -from typing import Any, List, Dict, Optional -import logging -from pathlib import Path from schema.manager import SchemaManager @@ -20,31 +22,50 @@ class SchemaIndexer: 功能: 1. 为表结构构建向量索引 2. 基于问题的表检索 - 3. Chroma 使用内存模式(EphemeralClient),进程退出后不保留;``persist_dir`` 仅作配置/统计引用 + 3. 默认使用 Chroma ``PersistentClient``,数据落在 ``persist_dir``(与 ``VECTOR_DB_PATH`` 一致); + 仅当环境变量 ``SCHEMA_INDEXER_EPHEMERAL=true`` 或构造参数 ``use_ephemeral=True`` 时使用内存客户端。 """ def __init__( self, embedder: Any, - persist_dir: str = "./data/embeddings", + persist_dir: str = "./data/embeddings/chroma", collection_name: str = "schema_tables", + *, + use_ephemeral: Optional[bool] = None, ): """ 初始化索引器 Args: embedder: Embedding模型实例 - persist_dir: 历史配置中的向量库路径(仅展示与统计,不落盘) + persist_dir: Chroma 持久化目录(默认与编排器 ``vector_db_path`` / ``VECTOR_DB_PATH`` 一致) collection_name: 集合名称 + use_ephemeral: 为 True 时使用内存 Chroma;为 None 时读环境变量 SCHEMA_INDEXER_EPHEMERAL """ self.embedder = embedder self.persist_dir = Path(persist_dir) self.collection_name = collection_name - # 内存 Chroma:不落盘,每次进程需重新 build_index - self.client = chromadb.EphemeralClient( - settings=ChromaSettings(anonymized_telemetry=False), + env_ephemeral = os.getenv("SCHEMA_INDEXER_EPHEMERAL", "").lower() in ( + "1", + "true", + "yes", ) + self._chroma_ephemeral = bool(use_ephemeral) if use_ephemeral is not None else env_ephemeral + + if self._chroma_ephemeral: + self.client = chromadb.EphemeralClient( + settings=ChromaSettings(anonymized_telemetry=False), + ) + backend_desc = "Chroma内存" + else: + self.persist_dir.mkdir(parents=True, exist_ok=True) + self.client = chromadb.PersistentClient( + path=str(self.persist_dir), + settings=ChromaSettings(anonymized_telemetry=False), + ) + backend_desc = f"Chroma磁盘 path={self.persist_dir}" # 获取或创建集合 self.collection = self.client.get_or_create_collection( @@ -53,10 +74,50 @@ class SchemaIndexer: ) logger.info( - f"[OK] 初始化SchemaIndexer(Chroma内存): collection={self.collection_name}, " - f"配置路径引用={self.persist_dir}" + f"[OK] 初始化SchemaIndexer({backend_desc}): collection={self.collection_name}, " + f"count={self.collection.count()}" ) + def ensure_index_for_schema( + self, + schema_manager: SchemaManager, + batch_size: int = 32, + ) -> None: + """ + 每次使用向量粗筛前调用:若持久化集合中表条数与当前 Schema 一致且非空,则跳过向量化; + 若为空则全量构建;若条数不一致则强制重建(Schema 变更后重新加载)。 + """ + expected = len(schema_manager.get_tables()) + indexed = self.count() + + if expected == 0: + if indexed > 0: + logger.warning("当前 Schema 无表但向量索引非空,已清空索引") + self.clear() + return + + if indexed == expected: + logger.info( + "Schema 向量索引已就绪(%s 张表),跳过向量化", + indexed, + ) + return + + if indexed == 0: + logger.info( + "向量索引为空,开始构建共 %s 张表(向量化)...", + expected, + ) + self.build_index(schema_manager, batch_size=batch_size, force_rebuild=False) + return + + logger.info( + "向量索引与当前 Schema 不一致(索引 %s 张,Schema %s 张),重新向量化并加载...", + indexed, + expected, + ) + self.build_index(schema_manager, batch_size=batch_size, force_rebuild=True) + def build_index( self, schema_manager: SchemaManager, @@ -250,5 +311,5 @@ class SchemaIndexer: "total_columns": total_columns, "avg_columns": total_columns / count if count > 0 else 0, "persist_dir": str(self.persist_dir), - "chroma_mode": "memory", + "chroma_mode": "memory" if self._chroma_ephemeral else "persistent", } diff --git a/backend/utils/__pycache__/dialog_classifier.cpython-312.pyc b/backend/utils/__pycache__/dialog_classifier.cpython-312.pyc index fbb9cfbd68ddd82d1465f054996c7acee8c0d0ef..3df1ffe6cce6c2f65f262557ea69daaa0728252b 100644 GIT binary patch delta 20 acmbOkGBbqxG%qg~0}wb1+}p^_rv(5yyaf6H delta 20 acmbOkGBbqxG%qg~0}x2RyStH_PYVD((*>^p diff --git a/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc b/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc index 3bb6df5fa24e8312a93e775321c66efce2da2f67..018318322f4de52a08cfbd9a050b24c9fe9809be 100644 GIT binary patch delta 3616 zcmZ`*dr(x@8NYX*?283q5f%(9i@^E_Ms1^3v>U}oL`1-e!Q$@1z2f3N$hj90hIAnX z#rVRW`l^qZ#I!h4ZANQR(frj;XWBm?+Cr{obSewdbjEcw)0xb4+V8v91>(fJv)?)Q z`@Zu#-}zqm{Hfx4b>d$w79#^sA@jhW<~)*kPj!Sn@=f{nXy@l+zv^*#y>-0k;e$0i z$MK}V<@G*l`(QQSG*8$O79Ha^PdvVKe&p5{ifS281iv8iA#r8E57g^5KI!lBdRG^} zQ?YhqrDx^Z@{JqZo-%jkTCdkp2;ekR<%oE_Px|*g=|3>icWA8rK=cNE-0|zt)4kC> zC!?L8jh(s_y>)b?ukG>Pt55n*8Aon*MsHDA`Vwey+1la_#pUjjB9S!m06*GwZ1lwK zX#Zi*ZBPvJh+z-s7j_n{_5}o4=lHdeTe)a`bWdmWcPAVxcaU(<2S+rHpV|}c?uvFF zRL)4RvR9AHFhqV1t~$=}Ocl#9RVt3HQX3cp!&x{Lr#{W%eDcR7a+(f(m4>r&+73;X zmP_JvFzPrxm%tfLt0pyZ#wByc4qcTVX457mTncCE&{idIshk-x49zN+jeeurILF0G z)|bvztHDUZ!@}PHe>F^>{aa1T)k|0{J)quT(zR-Lvx1RjT6J_peT+`iWNS4Xdr?I{ z)Yxga#jsPmi+Nw)s%bN{>dj0mzPDqz!6&1a{HI zPsTFMI#*ibZy=CH$BV~J>V%~eZ^(?7{!+(S|FN-i?a`Ax(MxAXZtuq;i7&%3o{OBH zEPYW&j?n|{BiB2l2ab)NyN-n%xpfq3SgEzqo{yn8NBZ`U+&uXBx1Wxl?H=j7G2VG_ z?AE1d|4}H($A7#Wz1{KhHnrhEfUgz(;gBcf3-U`H6NQS%hcAy^xB;yMXte9(7PJcAjSZdWRm?NdX zs`t6n68nav!44>?n|Yy>x^-^7izQf8^aEYq&l>eGQ!I2eVzMd+DNr1*xV%VWT`d_~ zH?FUCK&P*OMz6HR3NcK;;VovzWDts4^zhag5{q2v^pP%{6bLu*gd_s8q^Svq1GG%< zHd%`scJM)-_yQ~Ge!V;0AZY|Xu$^Ev5f_qtB#HEaetLmq;X}JTe0+tZukqFH5De_#$WMqD1IB6KjS=Lu`C*3`izGg6e&7Jgh zF-B9$8i8gQyDTvyW6m5fDg0KgHW;6Q^O#z2 z9{tSVcPxSz@+OcAELjYL00PEAisEb>?c=tW)`y$yw9`Ts`V55!SzJ%57h=5;o2yl3IjmUYFC1?x8fnVUP=Z&)QH_d zjO-Q!UqHO0XOhie;`9zNEJ+0cM zl5~U@8%an`0+)((gO;Sh4*@{er+Ay<_?WX5uE{`lnE*7kk{Xv{{AUOo&0z>eyO8HuF zh9n#Q!X+Rb%vYKf&;#>k54+~Y3~y?U(1PX^KDEH?;WS%JiyR%3=8WlamcZEa<68ED zXQ=f|MOgZFPCZ2a4>|u)`6>amMYXkBsg3pUCYPBK964L7kynNU>xxt$DFFf=laoW1 zV`)xNk_(NK)u|=#wXBmk-3vrA`UQVTNprJscgg7SY;S}F;XNJ#w{V8kAXgs<*VSRq%B2?y7P9l`*9G>-e3TfV z4oW}>-%g6^a-9H1_7%ZT0YA@ sRq0vHx6>Hb`G_&yV-oK%hOe3Q2XvQfsjW%HraWbkJ{PriRwqOLA9m*nRR910 delta 2377 zcmZ8iYitzP6`nggGduhAd$HZcUdPzZ#@J3AL>dfsU0CumcEHf2#%39IuFX2TJDWSR z_#rD!R67ZhBcQiU(xw$n6)8m^)XMw-g|xrYKdsbm6UlTSN-Chl>W?gfs8Xt`J!hs1 zQD?Q^o_o%B&zyT6cP~wRG#dUxDC8I5dHcqvY465U;j7|j!l{4kd*a5$m%n)J!k3qS zdgH_2-TKk5*5AE!>z(trJ~$2f*x|!d)JoFq5vuEyJs3ZH_{+;He_wg!#--((D{lht z=7&FC|KRN}&cEm9?~-=^%KAI6-aPsG&698NN%6v>2j3?MXI&w3s zT7FtmE6z$TlL&J{JT%NN`+9kUe~@ebc5(hO{u_ToEU#HdceI(Yl#$8Ml$ADe?y8Wp zK!6Ng_ZwUf4DbtqzH!BtOq!WwQESdu6kI(>6=;FVvujhZMjDGi?e-;OWU8U0uwP^BwIh+);hj8m= zgf4`~0Lqc@hpRS`L4KfW4qo=Bs(;HHD!^)!*zo#);IWk~)zgJ6>*Loq{E+P6$LePm z(Ua|+(O52>n{tlFwxM5iZUUN{9%HbuRe|Bi_DTf;xWKV#BZAEyJ&ox9Rv88N<=2C`EZBI6x zW0)D+qv^V>SVksoT5+FK0o!Y8*?fkYY&&k>fiz;;vPEYtlRb;N5rjPerVqevLqOKc z|JBe>o>|@2_Hqmm!sGjvaEe^Xa@TPi73wvL!v8ve+o7<6-AeER7V|GWdn& zeWaIrW8GwVb!#j^pr{KigW@yy;)oO5o6e;&1-Nvb(_o+hrkF}az8%h~qPTp>%D zW>GeVtPH{^S2wlp>H!hr&@9dIV#`@-X(nill*t$xN!mFnE8wPM04DxL+{-4W)7aEc z&hqn{K407drnopf99P`iLB>sZMSL9^$zwBVnS-XqCfs&ja@e-Qs8wJ&rw`&HTL!Hy z4I4QMCrQF$zJ3VmaIhmWo2M)d%|S<<(sxWawYQ}shLLePpxf;w^x%3U>$Ug}#YwE& zz}d2qE%TA~9YtxDEWo< zhpxoCO1`M<|Gc`cB*QOUTanh17bR6_?6`xrJKK4G$28RQOvgV(?J3wlFCLt5C*eD= za)!!HIaZVHL%^Y8I7RG9gh2pEj#F~Sj)V9Ok~3D{cj%mSHbqaMxBy`L(`Gv7Hnp3M z=XhV|17s^d)VXmlme^LI^w2Fii{UO?wX`WS$+Q`U{%pc*k)@qzZO@A|_y3~>9QINf zF0cNvlaUy@oe%GVUdP&Rjt%DKCfHj@a&DAmeyFRlZ3Wi0Kbf2?Krhf_l6jEu9BRE8 zV`>U-s3!jNu1855FLpI9K8O|{boUS#DPzZx?p%%dzsO<;2N1r8uphyxoDD^g@RAvtNP%?*VfXpyVNh+!kdbeH9i+>vbV;RR~`de4h&SpYiGLZFS=!sktp6 M-hD}GB#PtmzXxz(8UO$Q diff --git a/backend/utils/fewshot_chroma_store.py b/backend/utils/fewshot_chroma_store.py index 5f6e230..ee4791e 100644 --- a/backend/utils/fewshot_chroma_store.py +++ b/backend/utils/fewshot_chroma_store.py @@ -1,8 +1,9 @@ """ Few-shot 经验样本的 Chroma 向量库存储与检索。 -与 SchemaIndexer 一致:使用项目统一 ``get_embedder()``;默认 Chroma 为内存 -(``EphemeralClient``)。离线灌库脚本可设 ``persist_to_disk=True`` 写入磁盘。 +与 SchemaIndexer 共用 ``get_embedder()``。Few-shot 默认使用 ``PersistentClient`` +(``FEWSHOT_CHROMA_PATH`` / ``./data/embeddings/chroma_fewshot``),与灌库脚本写入目录一致; +仅当 ``FEWSHOT_CHROMA_EPHEMERAL=true`` 或显式 ``persist_to_disk=False`` 时使用内存 Chroma。 集合名默认 ``fewshot_samples``。 """ @@ -36,14 +37,15 @@ class FewShotChromaStore: persist_dir: Optional[str] = None, collection_name: Optional[str] = None, *, - persist_to_disk: bool = False, + persist_to_disk: Optional[bool] = None, ): """ Args: embedder: 编码器实例。 - persist_dir: 磁盘模式下的持久化目录;内存模式下仍解析为配置引用路径。 + persist_dir: Chroma 持久化根目录;内存模式下仍解析为配置引用路径。 collection_name: 集合名;可空,空则读环境变量或默认。 - persist_to_disk: 为 True 时使用 ``PersistentClient`` 落盘(如灌库脚本)。 + persist_to_disk: 为 True/False 时强制磁盘或内存;为 None 时默认磁盘,除非环境变量 + ``FEWSHOT_CHROMA_EPHEMERAL`` 为真。 """ self.embedder = embedder # Chroma 的 collection 名;显式参数优先,否则读 FEWSHOT_CHROMA_COLLECTION,再回退默认 @@ -53,8 +55,16 @@ class FewShotChromaStore: self.persist_dir = Path( (persist_dir or os.getenv("FEWSHOT_CHROMA_PATH") or DEFAULT_FEWSHOT_CHROMA_DIR).strip() ) - self.persist_to_disk = persist_to_disk - if persist_to_disk: + env_ephemeral = os.getenv("FEWSHOT_CHROMA_EPHEMERAL", "").lower() in ( + "1", + "true", + "yes", + ) + if persist_to_disk is None: + self.persist_to_disk = not env_ephemeral + else: + self.persist_to_disk = bool(persist_to_disk) + if self.persist_to_disk: self.persist_dir.mkdir(parents=True, exist_ok=True) self.client = chromadb.PersistentClient( path=str(self.persist_dir), @@ -68,7 +78,11 @@ class FewShotChromaStore: name=self.collection_name, metadata={"hnsw:space": "cosine"}, ) - backend = f"磁盘 {self.persist_dir}" if persist_to_disk else f"内存(配置路径={self.persist_dir})" + backend = ( + f"磁盘 {self.persist_dir}" + if self.persist_to_disk + else f"内存(配置路径={self.persist_dir})" + ) logger.info( f"[OK] FewShotChromaStore: {backend} collection={self.collection_name} " f"count={self.collection.count()}" diff --git a/main.spec b/main.spec index 7aae955..855febe 100644 --- a/main.spec +++ b/main.spec @@ -9,17 +9,26 @@ import sys _REPO_DIR = Path(os.getcwd()).resolve() # 根目录执行:pyinstaller --clean main.spec -# 收集配置文件夹和数据文件 +# +# --------------------------------------------------------------------------- +# 向量「数据库文件」/ 业务数据 —— 明确不打入 onefile EXE +# --------------------------------------------------------------------------- +# 下列目录为运行时读写或外置分发的数据,禁止加入下方 ``datas``(勿取消注释): +# - Schema 侧:环境变量 VECTOR_DB_PATH(默认如 ./data/embeddings/chroma) +# - Few-shot 侧:FEWSHOT_CHROMA_PATH(默认如 ./data/embeddings/chroma_fewshot) +# 部署:将上述目录放在 exe 同目录旁,或通过 .env 指向可写绝对路径。 +# SchemaIndexer 默认使用 PersistentClient(VECTOR_DB_PATH);仅 SCHEMA_INDEXER_EPHEMERAL=true 时为内存索引。 +# --------------------------------------------------------------------------- + +# 可选:仅当需要把 Schema JSON 等打进包时再取消注释(向量库目录仍不要加) datas = [ - # 所有数据文件都不打包,从外部读取 # ('.env', '.'), # ('data/schemas', 'data/schemas'), - # ('data/embeddings', 'data/embeddings'), - # ('data/experiences', 'data/experiences'), + # 勿打包 data/embeddings、data/experiences(向量库与样本数据外置) ] -# ChromaDB 在 Windows 单文件 exe 下如缺少 native 动态库,可能在持久化访问时直接“硬退出” -# 这里显式收集 chromadb 与 hnswlib(向量索引)的动态库/二进制依赖 +# Chroma **Python 包**在 Windows 单文件 exe 下如缺少 native 动态库,可能在初始化时直接退出 +# 这里收集的是 chromadb / hnswlib 的 **wheel 内 DLL**,不是用户磁盘上的 Chroma 持久化目录 binaries = [] try: binaries += collect_dynamic_libs("chromadb") @@ -30,7 +39,7 @@ try: except Exception: pass -# 某些 chromadb 版本会依赖包内数据文件(如 migrations/默认配置等),一并收集更稳妥 +# chromadb 包自带的非 Python 资源(如 migrations),非业务向量库文件 try: datas += collect_data_files("chromadb") except Exception: @@ -148,7 +157,7 @@ exe = EXE( pyz, a.scripts, a.binaries, - a.datas, # 单文件模式:将数据文件打包进 exe + a.datas, # 单文件:仅打包 spec 中 datas(chromadb 包资源等),不含业务向量持久化目录 [], name='Backman_Text2SQL_Server', debug=False,