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 9481539..7efc9fb 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/agents/__pycache__/orchestrator.cpython-312.pyc b/backend/agents/__pycache__/orchestrator.cpython-312.pyc index 852fe4e..8b01279 100644 Binary files a/backend/agents/__pycache__/orchestrator.cpython-312.pyc and b/backend/agents/__pycache__/orchestrator.cpython-312.pyc differ 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 ae331e7..df7b127 100644 Binary files a/backend/config/__pycache__/prompts.cpython-312.pyc and b/backend/config/__pycache__/prompts.cpython-312.pyc differ diff --git a/backend/config/prompts.py b/backend/config/prompts.py index 3a737e5..cc685d4 100644 --- a/backend/config/prompts.py +++ b/backend/config/prompts.py @@ -67,6 +67,7 @@ SQL_GENERATOR_SYSTEM = """你是一个精通SQL的数据库专家,有10年以 - **方言(T-SQL)**:字符串用 `+` 拼接;「今日」用 `CAST(GETDATE() AS DATE)`;标识符冲突用方括号 `[]`;非关键字尽量不加引号。不要用 MySQL 反引号或 `CURDATE()`。 4. **中英混排时的翻译边界**:仅允许对问题里**已经出现**的中文业务用语,在语义上等价映射到 Schema 中的英文表名、列名;**禁止**借「翻译」编造 Schema 中不存在的表或字段。 5. **严禁照抄黄金范例里的表名与列名**:范例中的 `TSBBrokerContract`、`MCBroker`、`BrokerID`、`SettleStatus` 等**仅表示版式与业务意图**。**每一条** `FROM`/`JOIN` 引用的表、以及 `SELECT`/`WHERE`/`ON` 中的列,必须在本轮 **Schema信息** 所列字段中**真实存在**;若当前片段只有报表视图且无 `BrokerID`,则**禁止**写 `BrokerID`、**禁止** `JOIN MCBroker`,应改用片段内已有的键与度量(如 `AccountID`、`CashSettleDate`、`SettleAmount` 等)重写,仍保持「有用 SQL」版式。 +6. **中英文同义一致**:用户问题可能已由上游归一为中文。只要业务分析意图相同(无论原先用中文或英文表述),你生成的 SQL 在**主表/关联路径、度量与聚合、WHERE 与 HAVING 条件**上须保持一致,**禁止**因等价措辞或语种差异而换用另一套查询逻辑。 **原则**: 1. 只输出SQL语句,不要解释、注释或其他内容(除非SQL内注释) @@ -369,6 +370,23 @@ TRANSLATE_NL_TO_ZH_USER = """原句: 仅输出一句中文:""" +# ========== NL 归一中文(中英文 → 同一表述,检索/SQL 一致)========== +CANONICALIZE_NL_FOR_TEXT2SQL_SYSTEM = """你是证券/期货类数据仓库 Text2SQL 的「问句归一」模块。 + +读入用户的自然语言(可为**中文**、**英文**或中英混写),输出**唯一一句**中文自然语言问题,供向量检索、选表与 SQL 生成使用。 + +必须遵守: +1. **语义等价**:不得增加、删除或弱化任何筛选条件、时间范围、数值阈值、分组/排序/去重意图;不得臆造用户未提及的维度。 +2. **英文→中文**:若输入主要为英文或拉丁字母表述,译为业内常用中文(如 market value→市值、single holding→单一持仓),与中文 Schema 注释、向量索引用语对齐。 +3. **中文→中文**:若输入已含中文,在不改变语义的前提下**整理为与第 2 条英译风格一致的简洁中文**,使同一业务需求的英文版与中文版经你输出后**尽可能逐字相同或高度接近**,从而保证后续 SQL 一致。 +4. 保留阿拉伯数字、日期、币种/市场代码、证券代码;原句中的英文表名、字段名保持不译。 +5. **只输出一句中文问句**,不要引号、前后缀或解释。""" + +CANONICALIZE_NL_FOR_TEXT2SQL_USER = """用户原句: +{question} + +仅输出归一后的一句中文:""" + # ========== 对话意图分类(LLM,与规则分类配合)========== DIALOG_INTENT_CLASSIFIER_SYSTEM = """你是证券/期货类 Text2SQL 产品的「意图分类」模块。 diff --git a/backend/llm/__pycache__/deepseek_client.cpython-312.pyc b/backend/llm/__pycache__/deepseek_client.cpython-312.pyc index 1699222..e418af6 100644 Binary files a/backend/llm/__pycache__/deepseek_client.cpython-312.pyc and b/backend/llm/__pycache__/deepseek_client.cpython-312.pyc differ 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 916de9c..90af973 100644 Binary files a/backend/schema/__pycache__/indexer.cpython-312.pyc and b/backend/schema/__pycache__/indexer.cpython-312.pyc differ 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 fbb9cfb..3df1ffe 100644 Binary files a/backend/utils/__pycache__/dialog_classifier.cpython-312.pyc and b/backend/utils/__pycache__/dialog_classifier.cpython-312.pyc differ diff --git a/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc b/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc index 3bb6df5..0183183 100644 Binary files a/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc and b/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc differ 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,