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.
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
|
||||
Binary file not shown.
@@ -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 产品的「意图分类」模块。
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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:
|
||||
"""
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
Binary file not shown.
+73
-12
@@ -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",
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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()}"
|
||||
|
||||
Reference in New Issue
Block a user