From bff5f85d603fd51c067db4b70a5993b9df1a9c43 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 13:48:44 +0800 Subject: [PATCH] Enhance SQL generation and streaming capabilities in the API server. Introduce optional parameters for streaming throttle and SQL stream granularity in `NLChatRequest`. Implement new functions for iterating SQL generation content pieces and adjusting streaming behavior based on user-defined settings. Update prompts for few-shot SQL adaptation and improve logging for SQL generation processes. Refactor orchestrator methods to support streaming responses and integrate few-shot SQL conditions. Update impact analysis documentation to reflect these changes. --- IMPACT_ANALYSIS.md | 80 ++++++++ __pycache__/api_server.cpython-312.pyc | Bin 50918 -> 53980 bytes api_server.py | 131 +++++++++--- .../__pycache__/orchestrator.cpython-312.pyc | Bin 33999 -> 39764 bytes backend/agents/orchestrator.py | 186 +++++++++++++++++- .../__pycache__/prompts.cpython-312.pyc | Bin 22348 -> 24519 bytes backend/config/prompts.py | 32 +++ .../deepseek_client.cpython-312.pyc | Bin 15512 -> 17476 bytes .../__pycache__/openai_client.cpython-312.pyc | Bin 14996 -> 17746 bytes backend/llm/deepseek_client.py | 39 +++- backend/llm/openai_client.py | 65 +++++- .../fewshot_chroma_store.cpython-312.pyc | Bin 11961 -> 11998 bytes .../fewshot_selector.cpython-312.pyc | Bin 27001 -> 28963 bytes backend/utils/fewshot_selector.py | 88 ++++++++- .../length.bin | Bin 4000 -> 4000 bytes logs/text2sql_api.log | 68 +------ 16 files changed, 586 insertions(+), 103 deletions(-) diff --git a/IMPACT_ANALYSIS.md b/IMPACT_ANALYSIS.md index 74c01bc..83b1027 100644 --- a/IMPACT_ANALYSIS.md +++ b/IMPACT_ANALYSIS.md @@ -592,3 +592,83 @@ ## 6. 配置变更 - 无新增环境变量;日志路径固定为仓库根下 `logs/text2sql_api.log`。 + +--- + +# Impact Analysis Report — Chroma Few-shot 黄金 SQL 条件适配(追加) + +## 1. 改动概览 + +- **背景与目标**:`data/embeddings/chroma_fewshot` 中存储的问答-SQL 视为已校验正确答案;当向量检索与当前问题足够相似时,不再仅作「风格示例」,而是以该 SQL 为骨架,仅让 LLM 调整 WHERE/HAVING 等与用户问题相关的**特殊条件**。 +- **涉及模块**:`backend/config/prompts.py`(`GOLDEN_SQL_ADAPT_*`)、`backend/utils/fewshot_selector.py`(`select_best_with_score`、`is_chroma_backend`)、`backend/agents/orchestrator.py`(`_generate_sql_golden_adapt`、`_generate_sql` 分支)、`api_server.py`(响应中可选 `fewshot_golden`)。 +- **改动类型**:生成策略增强(默认开启,可用环境变量关闭或调阈值)。 + +## 2. 方法级改动 + +| 位置 | 变更 | +|------|------| +| `FewShotSelector` | 新增 `_passes_filters`、`select_best_with_score`;`is_chroma_backend` 属性。 | +| `Text2SQLOrchestrator._generate_sql` | 在 Chroma 且满足阈值时调用 `_generate_sql_golden_adapt` 并设置 `_last_fewshot_golden`;否则沿用原 few-shot 注入 + 全量生成。 | +| `_generate_sql_golden_adapt` | 使用 `GOLDEN_SQL_ADAPT_SYSTEM/USER`,强调保留 JOIN/SELECT 结构、仅改条件。 | +| `generate` | `metadata` 增加 `fewshot_golden_reuse`、`fewshot_golden_qid`、`fewshot_golden_score`;失败路径亦回传(若曾走黄金分支)。 | +| `_nl_dict_from_generation` | 若存在黄金复用,增加 `fewshot_golden` 字段供前端展示/调试。 | + +## 3. 破坏性变更 + +- **否**。未命中阈值时行为与原先「多示例风格参考」一致。 + +## 4. 配置变更 + +| 环境变量 | 含义 | 默认 | +|----------|------|------| +| `FEWSHOT_GOLDEN_REUSE` | 是否启用黄金 SQL 条件适配 | `true` | +| `FEWSHOT_GOLDEN_ONLY_CHROMA` | 是否仅在 Chroma 后端启用(与 `chroma_fewshot` 策略一致) | `true` | +| `FEWSHOT_GOLDEN_MIN_SCORE` | 最低相似度(1-距离,约等于余弦相似度) | `0.88` | +| `FEWSHOT_GOLDEN_SQL_PROMPT_MAX` | 写入提示词的标准答案 SQL 最大字符数 | `16000` | + +## 5. 风险与回滚 + +- **风险**:阈值过低可能把不太相似的问题强行套在同一 SQL 上;过高则很少触发黄金分支。 +- **回滚**:设 `FEWSHOT_GOLDEN_REUSE=false` 或回退相关提交。 + +**回滚方式是否简单**:是。 + +--- + +# Impact Analysis Report — 流式 SSE sql_gen 分片与节流 + +## 1. 改动概览 + +- **背景与目标**:前端约定每条 SSE 为 `data: {"stage":"sql_gen","stream_kind":"content","content":"..."}`;需在服务端控制拆成「逐字符多条」或「与 LLM delta 一致」,并支持可选分片间隔。 +- **涉及模块**:`api_server.py`(`/g3sb/api/nl/chat/stream`、`_iter_sql_gen_content_pieces`、`_sse_stream_text_chunks`)。 +- **改动类型**:行为调整(默认分片粒度默认更贴近前端的 `char`;可配置回 `delta`)。 + +## 2. 方法级改动 + +| 位置 | 变更 | +|------|------| +| `NLChatRequest` | 新增可选 `sql_stream_granularity`(`sqlStreamGranularity`);明确 `streaming_throttle` 为相邻 content 间隔毫秒。 | +| `_iter_sql_gen_content_pieces` | 支持传入 `mode`;未设置时 `SSE_SQL_GEN_SPLIT` 默认 `char`。 | +| `_chat_stream_events` | sql_gen 循环按粒度拆分后对每条 `data` 可选 `asyncio.sleep(throttle)`;寒暄分支 `_sse_stream_text_chunks` 支持相同节流。 | + +## 3. 调用方与影响范围 + +- **调用方**:仅 SSE 流式客户端;非流式 `/g3sb/api/nl/chat` 不变。 +- **破坏性变更**:否。未传新字段时:粒度由 `SSE_SQL_GEN_SPLIT` 决定(默认 `char`,事件条数多于旧版 `delta`);若需旧行为可设 `SSE_SQL_GEN_SPLIT=delta` 或请求体 `sqlStreamGranularity: "delta"`。 + +## 4. 配置变更 + +| 环境变量 | 含义 | 默认 | +|----------|------|------| +| `SSE_SQL_GEN_SPLIT` | `char`:逐 Unicode 标量多条 SSE;`delta`:与 LLM 增量一致 | `char`(未设置 env 时由代码默认) | + +## 5. 风险与回滚 + +- **风险级别**:低。`char` 模式下 SSE 条数增加,带宽与前端拼接次数上升。 +- **回滚**:设 `SSE_SQL_GEN_SPLIT=delta` 或请求传 `sqlStreamGranularity: "delta"`。 + +**回滚方式是否简单**:是。 + +## 6. 验证与测试 + +- 已执行:`python -m py_compile api_server.py`。 diff --git a/__pycache__/api_server.cpython-312.pyc b/__pycache__/api_server.cpython-312.pyc index 9ad4c41f3d1e0fe53a7fc4bd446f4fbafa1cfef5..937744a2535672c8bf6dfcb3345baa4c5d1ef100 100644 GIT binary patch delta 13147 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 delta 10219 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 bytes: return f"data: {json.dumps(obj, ensure_ascii=False)}\n\n".encode("utf-8") +def _iter_sql_gen_content_pieces(text: str, mode: Optional[str] = None) -> List[str]: + """ + 将 LLM 流式片段再拆成前端期望的多条 SSE(与 chatStore onDelta 累加一致)。 + 每条均为:{"stage": "sql_gen", "stream_kind": "content", "content": "..."} + + mode / 环境变量 SSE_SQL_GEN_SPLIT: + - char(请求默认):按 Unicode 标量逐字符发送(与「用户」「问题」逐条 data 一致) + - delta:与上游 LLM 每次 delta 一致(块更大、事件更少) + """ + if not text: + return [] + raw = (mode or os.getenv("SSE_SQL_GEN_SPLIT", "char") or "char").strip().lower() + if raw in ("delta", "none", "0", "false"): + return [text] + return list(text) + + # 与前端 chatStore onDelta 一致:多次 { stage, stream_kind: content, content } 累加 _DEFAULT_SSE_CHUNK_CHARS = int(os.getenv("SSE_STREAM_CHUNK_CHARS", "64")) @@ -256,16 +280,21 @@ async def _sse_stream_text_chunks( content: str, *, chunk_size: Optional[int] = None, + throttle_ms: Optional[int] = None, ) -> AsyncIterator[bytes]: """将长文本拆成多段 SSE,便于浏览器逐段渲染(流式)。""" if not content: return size = max(8, chunk_size or _DEFAULT_SSE_CHUNK_CHARS) + delay = (throttle_ms or 0) / 1000.0 if throttle_ms and throttle_ms > 0 else 0.0 for i in range(0, len(content), size): yield _sse_data( {"stage": stage, "stream_kind": "content", "content": content[i : i + size]} ) - await asyncio.sleep(0) + if delay: + await asyncio.sleep(delay) + else: + await asyncio.sleep(0) def _conversation_nl_dict(reply: str) -> Dict[str, Any]: @@ -584,29 +613,14 @@ def _nl_dict_from_generation(result: GenerationResult) -> Dict[str, Any]: qz = result.metadata.get("question_zh_normalized") if qo and qz: payload["query_normalization"] = {"original": qo, "zh": qz} + if result.metadata.get("fewshot_golden_reuse"): + payload["fewshot_golden"] = { + "qid": result.metadata.get("fewshot_golden_qid"), + "score": result.metadata.get("fewshot_golden_score"), + } return payload -def _sql_gen_stream_html(result: GenerationResult) -> str: - sql = (result.sql or "").strip() - inner = json.dumps({"sql": sql}, ensure_ascii=False) - parts = [f"{inner}"] - explain = "" - if result.metadata.get("sql_delivery_message"): - parts.append( - f'
{html_lib.escape(str(result.metadata["sql_delivery_message"]))}
' - ) - if result.metadata.get("db_empty_feedback"): - explain = str(result.metadata["db_empty_feedback"]) - elif result.warnings: - explain = str(result.warnings[0]) - elif result.errors: - explain = "; ".join(str(e) for e in result.errors[:5]) - if explain.strip(): - parts.append(f'
{html_lib.escape(explain)}
') - return "".join(parts) - - async def _run_generate( question: str, top_k: int = 20, @@ -684,7 +698,9 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]: 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): + async for pkt in _sse_stream_text_chunks( + "chat", reply, throttle_ms=request.streaming_throttle + ): yield pkt data_dict = _conversation_nl_dict(reply) yield _sse_data({"code": 200, "msg": "success", "data": data_dict}) @@ -692,8 +708,65 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]: return yield _sse_data({"stage": "orchestrator", "stream_kind": "content", "content": "DATA_QUERY"}) + dialect = resolve_sql_dialect(os.getenv("TEXT2SQL_DIALECT", "sqlserver")) + dc_ctx = (dialog_block or "").strip() or None + top_k = 20 + loop = asyncio.get_running_loop() + logger.info( + "[GEN/API/stream] dialect=%s top_k=%s dialog_context_chars=%s question_len=%s preview=%r", + dialect, + top_k, + len(dc_ctx) if dc_ctx else 0, + len(text), + text[:300] + ("…" if len(text) > 300 else ""), + ) try: - result = await _run_generate(text, dialog_context=dialog_block or None, request=request) + async with _maybe_override_orch_llm(orch, request) as o: + chunk_queue: asyncio.Queue[Optional[str]] = asyncio.Queue() + holder: Dict[str, Any] = {} + + def _run_generate_sync() -> None: + try: + holder["result"] = o.generate( + question=text.strip(), + dialect=dialect, + top_k_candidates=top_k, + dialog_context=dc_ctx, + sql_stream_callback=lambda c: loop.call_soon_threadsafe( + chunk_queue.put_nowait, c + ), + ) + except Exception as e: + holder["error"] = e + finally: + loop.call_soon_threadsafe(chunk_queue.put_nowait, None) + + gen_task = asyncio.create_task(asyncio.to_thread(_run_generate_sync)) + throttle_ms = request.streaming_throttle or 0 + sql_chunk_delay = throttle_ms / 1000.0 if throttle_ms > 0 else 0.0 + gran = ( + (request.sql_stream_granularity or "").strip() + or os.getenv("SSE_SQL_GEN_SPLIT", "char") + or "char" + ).lower() + while True: + piece = await chunk_queue.get() + if piece is None: + break + for frag in _iter_sql_gen_content_pieces(piece, mode=gran): + yield _sse_data( + { + "stage": "sql_gen", + "stream_kind": "content", + "content": frag, + } + ) + if sql_chunk_delay: + await asyncio.sleep(sql_chunk_delay) + await gen_task + if holder.get("error"): + raise holder["error"] + result = holder["result"] except Exception as e: logger.error(f"[API/stream] 生成异常: {e}", exc_info=True) yield _sse_data({"code": 500, "msg": str(e), "data": None}) @@ -727,9 +800,6 @@ async def _chat_stream_events(request: NLChatRequest) -> AsyncIterator[bytes]: except Exception: pass - html = _sql_gen_stream_html(result) - async for pkt in _sse_stream_text_chunks("sql_gen", html): - yield pkt data_dict = _nl_dict_from_generation(result) msg = "success" if result.valid else "partial" yield _sse_data({"code": 200, "msg": msg, "data": data_dict}) @@ -856,7 +926,10 @@ async def nl_chat(request: NLChatRequest): async def nl_chat_stream(request: NLChatRequest): """ 自然语言对话流式接口(SSE)。 - 事件体为 JSON:分片 delta `{stage, stream_kind, content}` 或结束包 `{code, msg, data}`。 + 事件体为 JSON:分片 `data: {"stage","stream_kind","content"}`(例:sql_gen 时 + `stream_kind` 为 `content`)或结束包 `{code, msg, data}`。 + 可选:`sql_stream_granularity` / `sqlStreamGranularity`(delta|char,未传则 `SSE_SQL_GEN_SPLIT`,默认 char)、 + `streaming_throttle`(相邻 content 分片间隔毫秒)。 """ return StreamingResponse( _chat_stream_events(request), diff --git a/backend/agents/__pycache__/orchestrator.cpython-312.pyc b/backend/agents/__pycache__/orchestrator.cpython-312.pyc index 40bd50dc7a0ad07f818ccd0473b2216e9568ced5..081ed23046fd65c9d447e1103eb1d3079b1925a4 100644 GIT binary patch delta 12907 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 EUqw%O%K!iX delta 8206 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 diff --git a/backend/agents/orchestrator.py b/backend/agents/orchestrator.py index 935dd0e..99eef6d 100644 --- a/backend/agents/orchestrator.py +++ b/backend/agents/orchestrator.py @@ -5,13 +5,13 @@ Text2SQL 多智能体编排器 import logging import os # 新增 -from typing import Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple from dataclasses import dataclass, field from schema.manager import SchemaManager from schema.indexer import SchemaIndexer from llm.deepseek_client import DeepSeekClient, DeepSeekConfig -from utils.fewshot_selector import FewShotSelector # 新增 +from utils.fewshot_selector import ExperienceSample, FewShotSelector # 新增 logger = logging.getLogger(__name__) @@ -121,6 +121,9 @@ class Text2SQLOrchestrator: logger.warning(f"Few-shot加载失败: {e},将使用标准生成") self.fewshot_enabled = False + # 若本轮走「Chroma 黄金 SQL 条件适配」,在 metadata 中回传 qid/分数 + self._last_fewshot_golden: Optional[Tuple[str, float]] = None + logger.info( f"[OK] Text2SQLOrchestrator初始化完成: " f"max_retry={max_retry}, use_vector_search={use_vector_search}" @@ -335,6 +338,118 @@ class Text2SQLOrchestrator: return expanded + def _sql_chat_completion_text( + self, + messages: List[Dict[str, str]], + *, + sql_stream_callback: Optional[Callable[[str], None]] = None, + ) -> str: + """ + SQL 生成相关的一次 LLM 调用:可选流式回调(将原始 completion 文本分片回传)。 + 无回调或非流式失败时与非流式 chat 行为一致。 + """ + ds = self.deepseek + if sql_stream_callback is not None and hasattr(ds, "chat_stream"): + try: + parts: List[str] = [] + for piece in ds.chat_stream(messages, temperature=0.0, top_p=1.0): + if piece: + parts.append(piece) + sql_stream_callback(piece) + return "".join(parts).strip() + except Exception as e: + logger.warning("[GEN] chat_stream 失败,回退非流式: %s", e) + msg = ds.chat(messages, temperature=0.0, top_p=1.0) + return (msg.content or "").strip() + + def _generate_sql_golden_adapt( + self, + question: str, + schema_str: str, + dialect: str, + golden: ExperienceSample, + golden_score: float, + validation_feedback: Optional[str], + dialog_context: Optional[str], + sql_stream_callback: Optional[Callable[[str], None]] = None, + ) -> str: + """ + Chroma few-shot 库内 SQL 视为正确答案:在高分相似命中下,仅让模型调整条件/字面量以匹配当前问题。 + """ + from config.prompts import GOLDEN_SQL_ADAPT_SYSTEM, GOLDEN_SQL_ADAPT_USER + from utils.sql_parser import normalize_sql_for_dialect + + gsql = (golden.sql or "").strip() + max_sql = int(os.getenv("FEWSHOT_GOLDEN_SQL_PROMPT_MAX", "16000")) + if len(gsql) > max_sql: + gsql = gsql[:max_sql] + "\n-- …(标准答案过长,已截断)" + + dialect_label = dialect + if dialect == "tsql": + dialect_label = "Microsoft SQL Server (T-SQL)" + + dc = (dialog_context or "").strip() + prefix = "" + if dc: + prefix = ( + "【对话上文】(用于理解指代与续问条件;请结合「当前用户问题」调整 WHERE 等。)\n" + f"{dc}\n\n" + ) + + user_content = prefix + GOLDEN_SQL_ADAPT_USER.format( + golden_score=golden_score, + golden_question=(golden.question_zh or "").strip(), + golden_sql=gsql, + schema=schema_str, + question=question, + dialect=dialect_label, + ) + if dialect == "tsql": + user_content += ( + "\n\n【硬性要求】目标库为 SQL Server(T-SQL):禁止使用 MySQL 反引号 `;" + "标识符如需引用请使用方括号,例如 [TableName]、[ColumnName]。" + "字符串连接使用 `+`(与系统提示中的标准版式范例一致)。" + "「今日」「当天」等与日期列比较时,使用 `CAST(GETDATE() AS DATE)`," + "**禁止** `CURDATE()`、`NOW()`、`CURRENT_DATE`(MySQL)。" + "条件请使用 T-SQL 惯用写法(例如 IS NOT NULL)。" + "排版仍须遵守:关键字大写、SELECT 每列一行缩进、WHERE 续行以 AND 开头、PascalCase 英文别名。" + "\n**禁止**在单引号字符串字面量或 `N'…'` 中出现任何中日韩文字;" + "业务中文须映射为 Schema 注释中的代码或通过维表 JOIN,勿写 `= '过户费'` 这类比对。" + ) + + if validation_feedback: + user_content += ( + "\n\n【上次校验未通过】请在保留标准答案主干的前提下修正 SQL;" + "表名、列名必须与「当前 Schema 片段」中完全一致。\n" + f"{validation_feedback}" + ) + + messages = [ + {"role": "system", "content": GOLDEN_SQL_ADAPT_SYSTEM}, + {"role": "user", "content": user_content}, + ] + + logger.info( + "[GEN] 黄金 few-shot 条件适配: qid=%s score=%.4f", + golden.qid, + golden_score, + ) + sql = self._sql_chat_completion_text( + messages, sql_stream_callback=sql_stream_callback + ) + + if "```sql" in sql: + sql = sql[sql.find("```sql") + 6 : sql.find("```", sql.find("```sql") + 6)].strip() + elif "```" in sql: + sql = sql[sql.find("```") + 3 : sql.find("```", sql.find("```") + 3)].strip() + + sql = normalize_sql_for_dialect(sql, dialect) + + 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 _generate_sql( self, question: str, @@ -342,6 +457,7 @@ class Text2SQLOrchestrator: dialect: str = "tsql", validation_feedback: Optional[str] = None, dialog_context: Optional[str] = None, + sql_stream_callback: Optional[Callable[[str], None]] = None, ) -> str: """ SQL生成(SQL Generator Agent) @@ -353,6 +469,7 @@ class Text2SQLOrchestrator: validation_feedback: 非空时附加到用户提示(重试时传入上次校验错误) dialog_context: 前几轮对话摘要;与 ``question`` 一并供指代消解与续问。 + sql_stream_callback: 若提供且 LLM 支持 chat_stream,则在 SQL 主生成/黄金适配时流式回传原始文本分片。 Returns: SQL语句 @@ -360,11 +477,57 @@ class Text2SQLOrchestrator: from config.prompts import SQL_GENERATOR_SYSTEM, SQL_GENERATOR_USER from utils.sql_parser import normalize_sql_for_dialect + self._last_fewshot_golden = None dc = (dialog_context or "").strip() fewshot_question = question if dc: fewshot_question = f"{dc}\n\n【当前问】{question}" + golden_reuse = os.getenv("FEWSHOT_GOLDEN_REUSE", "true").lower() in ( + "1", + "true", + "yes", + ) + only_chroma = os.getenv("FEWSHOT_GOLDEN_ONLY_CHROMA", "true").lower() in ( + "1", + "true", + "yes", + ) + golden_min = float(os.getenv("FEWSHOT_GOLDEN_MIN_SCORE", "0.88")) + chroma_ok = bool( + self.fewshot_selector and self.fewshot_selector.is_chroma_backend + ) + if only_chroma and not chroma_ok: + golden_reuse = False + + if ( + golden_reuse + and self.fewshot_enabled + and self.fewshot_selector + ): + best = self.fewshot_selector.select_best_with_score( + fewshot_question, + min_rating=self.fewshot_min_rating, + ) + if ( + best + and best[1] >= golden_min + and (best[0].sql or "").strip() + ): + ex, sc = best + sql_out = self._generate_sql_golden_adapt( + question=question, + schema_str=schema_str, + dialect=dialect, + golden=ex, + golden_score=sc, + validation_feedback=validation_feedback, + dialog_context=dialog_context, + sql_stream_callback=sql_stream_callback, + ) + self._last_fewshot_golden = (ex.qid, sc) + return sql_out + # Few-shot 增强 if self.fewshot_enabled and self.fewshot_selector: try: @@ -431,8 +594,9 @@ class Text2SQLOrchestrator: ] # 与选表一致:生成阶段默认贪心解码,减少同一中文问题多次 SQL 不一致 - response = self.deepseek.chat(messages, temperature=0.0, top_p=1.0) - sql = response.content.strip() + sql = self._sql_chat_completion_text( + messages, sql_stream_callback=sql_stream_callback + ) # 清理可能的markdown代码块 if "```sql" in sql: @@ -609,6 +773,7 @@ class Text2SQLOrchestrator: top_k_candidates: int = 20, include_schema_in_result: bool = False, dialog_context: Optional[str] = None, + sql_stream_callback: Optional[Callable[[str], None]] = None, ) -> GenerationResult: """ 主生成流程 @@ -619,10 +784,12 @@ class Text2SQLOrchestrator: top_k_candidates: 粗筛候选表数量 include_schema_in_result: 结果中是否包含使用的Schema字符串 dialog_context: 前几轮对话可读摘要;选表、向量粗筛、SQL 生成与无数据说明会参考 + sql_stream_callback: 可选;SQL 主生成 LLM 输出分片回调(用于 API SSE) Returns: GenerationResult对象 """ + self._last_fewshot_golden = None original_question = (question or "").strip() translation_meta: Dict = {} work_question = original_question @@ -726,6 +893,7 @@ class Text2SQLOrchestrator: dialect, validation_feedback=feedback, dialog_context=dc_raw or None, + sql_stream_callback=sql_stream_callback, ) last_sql = sql except Exception as e: @@ -770,6 +938,11 @@ class Text2SQLOrchestrator: meta["sql_delivery_message"] = None if dc_raw: meta["dialog_context_chars"] = len(dc_raw) + if self._last_fewshot_golden: + gq, gsc = self._last_fewshot_golden + meta["fewshot_golden_reuse"] = True + meta["fewshot_golden_qid"] = gq + meta["fewshot_golden_score"] = gsc result = GenerationResult( sql=sql, @@ -792,6 +965,11 @@ class Text2SQLOrchestrator: fail_meta["db_execution_status"] = last_db_execution_status if dc_raw: fail_meta["dialog_context_chars"] = len(dc_raw) + if self._last_fewshot_golden: + gq, gsc = self._last_fewshot_golden + fail_meta["fewshot_golden_reuse"] = True + fail_meta["fewshot_golden_qid"] = gq + fail_meta["fewshot_golden_score"] = gsc return GenerationResult( sql=last_sql or "", valid=False, diff --git a/backend/config/__pycache__/prompts.cpython-312.pyc b/backend/config/__pycache__/prompts.cpython-312.pyc index 8a4418c01d64703011be1b7a7bcf6b511eefb5f3..4eae5899154c5ec51cbc3b10b9bddb411ce44cde 100644 GIT binary patch delta 2291 zcmZuzO-vg{6t>f-srZ|XvVZR}|8?s9eAxXreF(ZIGiyMJ_cv#0Iv!rd8GO?dFw5)NlkF{Vf{ zlwI2v`WePzQDhknrnG?&p2%4&JEepo4dHu*Z6Snb{K)T9#ZjCJkdWI#7*EY>{)Z?! zqWj$uBa&#bp!H=ulhPlCAPH^DZ?V8$ij*KxLj%g@&`v3&$}xN&CWn!40EJKQ0_Tu=O9?x$J zeOi720>D5WT*E>i3>0}zogo8NOzJ`qMdr|^M-_ummKrSJ>>!>>DKcOXMvIMJIfNV=F6zK2`q|KR3sS{mML-+xnMpFswOAXh=j|Me0&y&PQFx4$-IqYoKrIURRN12ovbH(0 zQ%b^~QdnE)(-RTwiQr@&hi4&#mU3r|VB{Gd^5Nkiqzq%0Y-S*&Po(v!`xIferTshS z4{fX(^yBQRB8k8mDOnsxlNl{Hucb0Or2rPw04dCrmm{AS^(FA6fR3Y{7s{ z_lMQ~g0?h($MP)q)g^1Q70|j=ckOc1MFLxMy|tPB{5lNnwyPWqdw{^u*J50sn$`Ug zfC0zn@#HiL6{#Gk3Ml_n8nu9|T3o25vH%@`PO+Q@e9@BcS^1IRv472{+|P*zMZ$P| z1_hS1tqG-=r22teNSPmyGRqLlQz%zZ3@I4p0k$X_hG;-G45+ebBUM`M1uVH&0VPOK z1%Aa@&F|BN86?JmKG;zks_2l)P{h+w5`QvNp}4{?bPEmz={yhtEeNs&K%f!fCnW!K zB!9yw0Nr6V7(k_nn@Sp5R+6+uk2*RHv?`LI`Sbq>qwogm_0cS(gwl%;lN6XvXsM7| zdZ2A3;dYlwR+UAVhSp)Zx~6&%eMvxR(zg1fK-3#wW3RTk+S@wMx4E3GHuMxpKZKLObtjtr7;v)hcsF4{Flx3b$09NGaPHQc0cnWDTE>hP(W_#&kKsQ3jMxlc44O4vpDmx{25)1&OQSJr2lF|KxRD5(94%{=k4GPTD z1|s0C$f`VzBT+S7B&OMV(b{BfuCv>k0a^myZfAh=YMU|SFH)FgFTH~`pe)iu?y)>r z6Amr{b|et-Ji`gJ#W_d>XuI>~K9gx%h?X7cf1d{92pA^4>(QiN$|g;{m%2KE*RWLZ zR<`Hcu1?;0v(?q!)$RQBtoz)JUf5Gx8wBr#Z&Vzm&PKf)%3wg}FuT{T;rJ+rxpl|s zy4}%rvzK8wj&t4WWXexq+)L&d29ke)@}jPlNcC zFnc>0f-pUMk|02p4?sBIw2S~NdiajEPG|dV2%_cIpP|4F9DpAgT>)i*NG3CTkw()+ zH6?1I&qUCOrB;2JD_FAVjgt?6J+I|v;RHa*O~RVdqVV)NiRo~1_(v!saGt@LyHfqJ?G5wsnA85frRJio zq2AimN@Q7AUw75s+H$?cZf*Rl`uPVdzuF&|nYZ9?h#5YvKV!YuJ~{mADXaO3spjd) WgD1Xu>4~ZO>D#f@Yq9+-toRSX)HzxJ delta 134 zcmX@UpYhB(M!wU$yj%=G5FdL#lRIJ}p9EvYM0H1-6uuPx6oC}MIV@4UsZ6PSseEZn zl0apt{3${OT>PR+U1V3U?DlX^0I}t#C@xMx|=|-mI~mgd|c| zx4)fv@6F76Z+`P}C)(aTM7+QCc$@+}(>A=Hh|cfzhRMgh6*bi;_kg%usCEA zmI<=sfFL_xHI1smT;>9qdrX$+5M;mXQ5;Hs9+=5qCEQ?Bz$BCXL} zRD}_6D4LH@fM7$g0Gt=@0Sf;rW?L;KRzHgUY`>*zQW-A-)WbXWct%V2))V%R*|lt& z7!2AJmn{RTs3tq4STYH0D_CUZs2p;4t%bLB> z2ey7lOjbXU10dxZ_+~3vvN3|a!7b+;u*L#%P|lrT6`Ek-l*3?=&3nkhex-cqH;318 zT#H-)_27hhA=Go*ilmav8tZQ2%7PduPisEAW~&WKSa%^z)a2Xq^D*t`O}a%@k_{Bi zRGjrlgqE|l_9LV$^R9i8xC-ZmPsqdOLbB^(s!3LobT*oOr7bOB%e`&YQ@~IP^YQq^ z>*hv9`?{9c`E+*QyMWAKbA0QpvrsUb9rhJRYOq)fpc6&4!#6Ve&~ntw&s_DrP0HwE zsML?L8j6D8hIbmg_s*FGVU<9HK7nm;gxKb}=h^QZ@0nyPi#tQa#)Z1GP_*|uI?TNG zs4JV5xn?>YIIIJfR6FaL7i6kyVU;%_w42Y8qol*qZdoF1T_y+}V!ODr5G+6UsHEGE z8kXhGu_E(!7PF6BInT6PK{H*O^uWB`D!X9yftUDzuDad2v(%_f#FtEcgb3X$EJB-m zvY=s}`7=eJ#T_;cv%#O863Yq)*#&o?)7@d)YilK~!XERtgjVu%b1RX2EiGW?e2~TA zi~EKKj@{n({LtC+cLz=lcOQ<_H>H}B3a;YF(2st2=ispqFaLDt;N{_$4i5MK?AGCf z!>?VuJ83@~A@z-iZQ^-XC_No#9cB2ZnTD9sx;E6T1&-TD=yF~0HK ztryM>o&Ck_3qPrjP|nlfI8W(39Qtb*jwaLDf98ccbrDvD7T0aS?zpB=x&n1|2s02K z2axR4ATkP&pmYhsasXYd-_erZMOUI=DZm%0t-CP2+l*+R&F2w8K#P+Jh_Wy`LAeXt z?}pv}w(x0}FcM&Y^)WG2Fc>Og`~11)0&gih<*(>8IURw);DVk_eOr3B+z6Ik50>2s zR$dQQ-Uu$Zj=#q*%>y{J^qstt9&s>K*w=BqV=$+1FkE!nbJBAremy+(MmTys9PJVH z>~Q}#&pdf<#T(imi)sh+OHR)?IpYsG6(6~T{K%&ty9IyV-vyH+@UA;>Z1v&QZxz&B zT32_e>aDy*Z@V8GbZ6Zk5kdQJbw0Shf63Hk0rKXwx}v31g)0sZV#tqJK|pb3s$-eY ze5EpPnbUmLDMI`OGqUJe z>-O#2)!oTER;beT2pa$-hwcTcHY6IO%@kIerqVd-o?*TDVRS$#g1Rt{XGRyaxqg|P)gts*jD&^(pit@GC~7@ zWTI&-^T9qF9o>O?E8U6uW`x}c$UvGzcnUzW86J;!!kryAM`u+$;YS@afm6RMj*{E# z&&7L48H<$^SsSr8HM76uH=lR!6A@HXhx^V*Fd^J*Y@o(dC-M2yqC2o_V;p1a8GPbC z$VQWfoa3}el8ms-C=NyWFR=|nReRezflA?3_ly3%ALELQp= zL@`pDPxfY9lYeZowqgf+*oCr5UrhD*VCd{<1lJZB$1$dxpU#QLV0}wYBZw}P?n6M9 z=;ox7)~ytF3UFn7D#l`yT*iFhNHxaP?#0GDqAo09Upm>X6V)Kncc8Ai07fSWKQKqf z%3~1MU&`+TZ-y&ErY{NR5O(<$ym=n=-$xjCb|P3vjcm`)1dX(uxnd`akIU0qh~1 zHb=4LF$B!`=eVOg)#hYEOD58aDwR9NlTF8)eHWYYMT)tl?d-8=*otSiUdno-rK~Gj zMN;fS^mRzEJaZuLIH-WX{QSUtjNje9F>5t>j(u-daVOq@*GfdUXt2#lA&Qp}T7*!H zP=eq`s6^n~kOfN-1dNq(ikPv4Yl^Nxz?q;a1d1>jp#$L{LN@|BM_<4@3O`0EI$~VM zreX;p0jX~^FB5S2WgJS(qrJLE} zni-wc4rSdX$F+EUGOntMN*${JGkC~7 delta 2982 zcmbVOTWl0n7~ZoxJKgSfFD<=oZDP5WZLbV;m4%-xzl z9f$?e!B|ktWi6C0jg_X$Vr9hPbWqpfR$S`wiYP_q37HRMC3#tOu|o~1ep*DsVK7qzv??j7!8v!VhS!nNI=3YarxKA2it4;5FWheGNqSTtB&LeV!!~6?i$O_h4a^rUnAgI*e7a6)$c@R9#23O$MfJn{`=Z{8Hk>2R z*g{Q*r=|wGbH!;%2Gx=b%?zYOVQe}Z5mW?`d zgk0x4T91+zezUdSHwJ2^Aq?hzZT<4e82`X?r0wVPmlSAPfpNdx-y`TOT9aSPB9j&|Y&MyoHhci{UBU;$chG9$ zoslNq80jVszAN$&ShQnhmGfV+dL4Ip28-TA{TxCe>V+&CLzP%Wgjz6v8jJ39u7VDP zyN*dM;KqNDD|4T8?Q*UawSR);#O96QwkRA04fp$B!17ey=i zy*1rrGjE7q14`YBeinWUoPgQlcpHOp;b_0Sc7(jl@2_o`#R_awh$WeD2q_h;7UrxD zp&p?DA&9UFK|*jLEJpAnhy=yyg$-dzgb9QugjW%cBb-3Mkl1O2W`q{;-AB5xWsjf{ z2F5%5+Pc2nlJ)zE7b6tf#M#mc{(jFx(#FqkI5)5yU~w?Ql;C_03;!vF>zQlht_uPn;%Y^*0!T<-gH$N%hxU0d8hl=cjc z5GeQeKUX1=6|xX2x&RV{hr;DKqyJ+f3t=-~JlN188jp4h1Q0a-CJffO zFTm_|{>I==vA9CEDYZ6r&Vhn$=l2FXW-Xr@H{%H{ZWz>H9#kN!Ejg2=dOT(So?s)b zQd<%+r?b?EBo4tpBYvO+-K=8xQy9sNk5guIwH-D2Kt-B1IWeYrmf>i)*j&2JAygM)4|Kpr^6MTT%h@=fAv{>t` zEw(xv@9RSL7Dt_vtHL{F&5B@SRMuBV1JoaFr2`+4y(*sr zWYREsj_ZqWDM``*O*>ALoRSEA9gHN8 z)0DU5lC)r*UUEx1;4n-bl~N=<(2P>5WH&&=1Pvp+&5}tnv%iQb`Dv16kG9StB}mre zWa`wo@N~%r;|V+MKF>g+_A_F#qfy*Z>CC8KVKZTWDsiJeH5Gk$MkQ+^flw2j3!Mmt z%j>1N>~Z~0k~3g7EES21)h48^;;x>-%RZefwzNutkgRVBL?Zs?Ktu`447?DL=MwHC zUo)+>Y*98W-&4DOiTZ!XjSZACMb7(%>W?e`M*g5N}d>asCbz5tjECwiTr9Km# zhdc|Au6=mBpEgGn!KKV=OV%#N!6j^oEnS<7&GLbbwsR^6dM95H$jvRHAlxAkp_kc= zZnn&@mZ({e;Z8OOdZv;Lw$WI?DvhOV5c&t;tu?vX;gUp_QKDB#dS*Aeh=F}*aIp+i zy4rYL&u%ld6BDbp)Vpj!p;J6f4v{Wxr*?(#;A%nW(skv z@6-dWAUX>a<4>nvvPkN)YNZEbi#qjNvf_hNUPn~Dga`+#HNsAHwh+bK=1-15KkPC@ z^Tzv~hVh#1GD-=Z#%%(9Q6l_(UQ#)D<5@}7DQ+XQzf;`eV?CBcHIdY;!t(I>SO>`>n?Y$wmDw-cYe zy%DBded*EB!NZ?CdSLYQnd^gnR}b#@Hnz63g#yuFYuG#b%M+jOJ$&VrXGZtFa`njG ztH+=Hc>mt3&%bniaNm^+hrErO{Za2|_rcM_yRQ!(9DVYY(dYZTktmG?{4KX_pzVR{ zgZuxvYj-^7V70X~Rl^3pLHYD_K5+14orz1Gx_WAsPRYxe@I zYl{WKRn^|AyQ<^Xoqz1(C;CTEzwpV6zgpzYjZj`~E?Nf6-?Q3qMmfSlgbIX30I~=* zAN9*Rs9%3H8l&_M9I8UVs-(*h3J{h9Of5{h0;zo4F2&YLgc<-@Y~0))ewfxG0qR4b zYXBlxo!-fZV&%#vj6{Rtpbq!Cxv;@kRWDce_o+iDU=z6Q6XB~}!kAe|PG`GqPiMQ* zMqHlWj-HNFzWxp8>d({<7gi1xR$g*d-AJ)y8TV<&(%BmOvY2yW_lDm3p88>D?vOKg z*jX~$kt$IHtvs)Z-rT2C{-ZhexKH~PAv>daX@(;P^47-bl+(q4@ zoajD&@2PwGtItQ@^(+}l%{*CntngA&-VKwG>iy~qvtUd4Tu3%JK5#g@{cQHW<(F-F z{qu)x%;Q)PGul%|Je5Z`oUA`qKkO+S@)QnxDuz52!=B0^{HuAj44`|}`?K(~{u<8bZ%+ILbGUs(S)^U@Ks{hP5uQ1(BO z&EVbLEA=(=$sbHrp4EEck3I`xxef4j9f5(@^@f^a_3QT1ntb)2G*zA&QT0Za1+mx$ zcu^36>7r<;v5OZIRKRr6MzC#9#df|}Q>;^8ELH>4uM_vH3_KGt)7TMb!5U02SqHm( zI2dk*a+=utl@#N&^bUnN}w3{|yVl(VYlfwMq5~)eLdnK((hbi^#v(JDFXe+FI{#NI3?X-r_UD^`kr`&`F=d zby)DJr|jFMY;jgf3-??n3g|-M!R9)aW0xnRG13+aMnl1HAmTG9uH^bo%#z7gCFW~j zj;x6Wc0^?j=QjW$t~ihOYgzZH%!H+7pMz+>nY}vYF<|hiCWTJk=<;)FGBOAR2CrDV zgb{1vu(fQ+T6TWNC2Qq7`bwHJz;afT*bK&h3AZ9)&eh&iJ}{W8IuZKjzGL{jx0F7j ztjvRk+d5Ou_7x{VUCZ@}#NaN-}i@zZf4z^9Y=i8?P z>zlnsHH)w-vmb|mZqK`2rR6sLoc-44ojL6&w+B^^+w;a!<%)zRAy8Vu216Gv+)!hQL?EVA4K-4$Js=RXynbpbw@^ zaQ3lz1sSnc?DNx{M}@ce2E|(_gO9=*+<~`n^6@8u3C4MV`8co2$@zR-Gi7{IsyQ}= zJktU^OOjEG>4qsv8E2f$J^z2_SZM}|%S7-BF~C{aEJon*T!$^(1Zf*WJ3 z!Y>exAY>z8c=(lt=3xtA4*ZYIf`r;5+)#<4{$_$nG~P^IEIP(I$ZewIvSap`2=C-E zt&o^Jri12~UNE|D8nE>Ud%5tMmW$0Bc(%w)%6%XYd^{KB8xiaX4g~%ooR6&{fQj7z z3B~OCyiBr@S&QdGoh&c@h{SmDx^NJ`zI@`X_tS|Xei2nJm1bpK+*Oq8<#ZvGFV1~8 z{Q^nxbcQ9SoY~y>{5FV(T?7-AZ+43N%GqnBX{3q$z4Wc+y|~&@6ri0JF`(WOuv=nq z0w*HWvupF+iWg4|m=;`Ck@f7evQ;khnrvNn*UGh3)eXz4?q1n&&yQ%S4XthLTDe&~3pC39TmEG|&w!soH!iuuvaSt3Vf-Op z&bQVC*CCC&M>aMzG_^9TTzlk4M@}~Kbs1h*h1*SWK~!qcN5>zBY!2JNB1?R0>BUg6V}y(^Tl Z%thsvHVw(YAt3&%$-o*H6>8AR{{a_$CV~I} delta 3306 zcmaJ@du$xV8Qg5Tib^SMK#tKklEd3E5x9FZuYsu9YEli_?M z84+ub(UfmaHs_c)EoX^CNw%*3cGFawEbWn~e?_8!vvQTuE@(lZ zg=(}8pF|@*Y5k8j(eR3r?Bw0#H$k`U+Fe+*-7~q2we}zKr2JqrSYlCx7KAp0c7#rZ z7(y39H^3R`Dxmbg?xX3 zm$p+6(0sImdSUd#=z}pp{WS0*@$nCowm>HhuBgc%4bd>aqO=5Qj7ESET5Qs~)+W?{ zlRy*tEV9zYN4zslSX9_te?N?T_ju9622*gwDCf? zt5xK6V@ml;;r2iShriW%6Mr{+A6epqk;!d+U`>nJuB>he^G;prWpd1+5*gw@i0mN4 z{7;d&JrPi|U8Q2lR&>USObf6Ckk~lV)#Hz)S=Mw+jR{IA&C+?(_A=cp6$_@$ZbRvv z{Nbi@f0@0JgeNWcjpZ`%T@V`a-&FSPbJ~ z=pH+Oa1cNXiWuxk)iJ?R$VnrJh-wB!M%fH9goQ&myPsd_ZzcWQvvtJtJP^zn;k$?;Y z0|~-g;b>hPOK*Ars@awy-RA~I$gj9M$e>Xd2fgG|KCyXFu>Ds0sG+BfoM}0iL#-g! zxmE;ORzkS3YiNs|KoG9g#cQtl>Yd;lN{~1Bj-e;OwbzDTBNePT+mJIN{A0o$OwuK| zLnY1SI4O<>ws$!U@c#0%?=L^sH*jJ3#S6qtH+BkwJ3qEdb_#hCBrc(!WxUJ$@p$ir<)01y^Yvd}eCih$PybTeSZTQ- zW>l-j{QD{A8->MZ7;)}S_*80sC+w$?`ZNMo*LE3t!B#U2x){8#_h_x)pU%X0P+8=l zZtH7kiet6iUB3Zs6Cg1}hEZ8*L!kq7^NR zykJ|hy1M^{+9HjgsS{?K(8y+l9trHTUYw31h}z)&#*QK65f}m9+;RcGQMs;>Vr&5E|Y9xEw-=1Jq755=QyYclMAT{=1#y(D)zi{0DKqWwm#erm!!d zpeVmMu{9Qxk%7^`cS!6TMZ&EJ8zjZ%B1x0T-HxyW0l&GhMFQtyi)oSHyKp@NU}E9K znH8s%P4J~Vy2t=OeaAcL=TYc;01$R#X@J^`ta@cOWc)O85xU_Nn%HkE(m8khs#{h3 zA2j)tVW&E$*WTXso=o=gpYEP(Zw4tla^T?X{@r({rgz^vn>zHBeJsl5>8%x!X^{u7 zEf-B&g=dgnc+@%0{`=ELS)aw{4Z9PKxG6klomVH89-J5Y(=d15ahOHhpIOMJsxKhS zt*HWtL{#ssmJHL=mYyn>nj0Rp>}3!e=dVv+J0hCn30PKlSPC5|S-D~%Z5)BC<*OUd zh43XNGH3f!sfDstW_l{cTwsBH8Nl`!#cUQ{PV6P_nHj6ts`Hd~y3LOg%?>4DEtYgM zkvW#O7~Z)IuMpb@S=k2`;H4+3HH;j*X`B-vih|y6I3Kq6_I#0+4Sg3o3tXs}WUd16 iH)l4vN8Xi2_}^x>hu|ePd__Y1?=cU*c{c9G@IL_(U8hz6 diff --git a/backend/llm/deepseek_client.py b/backend/llm/deepseek_client.py index 920b700..205abd5 100644 --- a/backend/llm/deepseek_client.py +++ b/backend/llm/deepseek_client.py @@ -6,7 +6,7 @@ DeepSeek API 客户端封装 import os import json import logging -from typing import Dict, List, Optional, Any, Union +from typing import Any, Dict, Iterator, List, Optional, Union from dataclasses import dataclass, field from openai import OpenAI, AsyncOpenAI from openai.types.chat import ChatCompletion, ChatCompletionMessage @@ -104,6 +104,43 @@ class DeepSeekClient: logger.error(f"DeepSeek API调用失败: {e}") raise + def chat_stream( + self, + messages: List[Dict[str, str]], + **kwargs: Any, + ) -> Iterator[str]: + """ + 流式聊天:按 completion 增量产出文本片段(与 chat 同参,固定 stream=True)。 + """ + params: Dict[str, Any] = { + "model": self.config.model_name, + "messages": messages, + "temperature": kwargs.get("temperature", self.config.temperature), + "max_tokens": kwargs.get("max_tokens", self.config.max_tokens), + "top_p": kwargs.get("top_p", self.config.top_p), + "frequency_penalty": kwargs.get( + "frequency_penalty", self.config.frequency_penalty + ), + "presence_penalty": kwargs.get( + "presence_penalty", self.config.presence_penalty + ), + "stream": True, + "timeout": kwargs.get("timeout", self.config.timeout), + } + if self.config.extra_headers: + params["extra_headers"] = self.config.extra_headers + try: + stream = self.client.chat.completions.create(**params) + for chunk in stream: + if not chunk.choices: + continue + delta = chunk.choices[0].delta + if delta and getattr(delta, "content", None): + yield delta.content + except Exception as e: + logger.error(f"DeepSeek API流式调用失败: {e}") + raise + def chat_with_json( self, messages: List[Dict[str, str]], diff --git a/backend/llm/openai_client.py b/backend/llm/openai_client.py index a2a94dd..62004e3 100644 --- a/backend/llm/openai_client.py +++ b/backend/llm/openai_client.py @@ -10,7 +10,7 @@ import json import logging import os from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Iterator, List, Optional from openai import AsyncOpenAI, OpenAI # type: ignore[import-not-found] from openai.types.chat import ( # type: ignore[import-not-found] @@ -119,6 +119,69 @@ class OpenAIClient: logger.error("OpenAI API调用失败: %s", e) raise + def chat_stream( + self, + messages: List[Dict[str, str]], + **kwargs: Any, + ) -> Iterator[str]: + """流式聊天:按 completion 增量产出文本片段(与 chat 同参,固定 stream=True)。""" + max_tokens = kwargs.get("max_tokens", self.config.max_tokens) + max_completion_tokens = kwargs.get("max_completion_tokens", None) + + params: Dict[str, Any] = { + "model": self.config.model_name, + "messages": messages, + "temperature": kwargs.get("temperature", self.config.temperature), + "top_p": kwargs.get("top_p", self.config.top_p), + "frequency_penalty": kwargs.get( + "frequency_penalty", self.config.frequency_penalty + ), + "presence_penalty": kwargs.get( + "presence_penalty", self.config.presence_penalty + ), + "stream": True, + "timeout": kwargs.get("timeout", self.config.timeout), + } + if max_completion_tokens is not None: + params["max_completion_tokens"] = max_completion_tokens + else: + params["max_tokens"] = max_tokens + + if self.config.extra_headers: + params["extra_headers"] = self.config.extra_headers + + try: + stream = self.client.chat.completions.create(**params) + for chunk in stream: + if not chunk.choices: + continue + delta = chunk.choices[0].delta + if delta and getattr(delta, "content", None): + yield delta.content + except Exception as e: + msg = str(e) + if ( + "Unsupported parameter" in msg + and "max_tokens" in msg + and "max_completion_tokens" in msg + and "max_completion_tokens" not in params + ): + params.pop("max_tokens", None) + params["max_completion_tokens"] = max_tokens + try: + stream = self.client.chat.completions.create(**params) + for chunk in stream: + if not chunk.choices: + continue + delta = chunk.choices[0].delta + if delta and getattr(delta, "content", None): + yield delta.content + return + except Exception: + pass + logger.error("OpenAI API流式调用失败: %s", e) + raise + def chat_with_json( self, messages: List[Dict[str, str]], **kwargs ) -> Dict[str, Any]: diff --git a/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc b/backend/utils/__pycache__/fewshot_chroma_store.cpython-312.pyc index 018318322f4de52a08cfbd9a050b24c9fe9809be..bfe91373590fb0a3a7728cbbc937278faf47a3c3 100644 GIT binary patch delta 67 zcmdlPdoPynG%qg~0}$--d622Bw2{x8N6pLGDkiizwWv78$i%?VGR7sfIJ+djASNj> VIXgEoPd7O+H#H|_vl~yb5&+jg79s!u delta 30 kcmcZ?yEB&WG%qg~0}$vl+|LwK-pFUp!>GDBo2OU_0ELzZ<^TWy diff --git a/backend/utils/__pycache__/fewshot_selector.cpython-312.pyc b/backend/utils/__pycache__/fewshot_selector.cpython-312.pyc index 99f945767174c35d9548543bc56024cdb5e65e9a..28f8ee28ecf58f640f3aaf8711df50f543433165 100644 GIT binary patch delta 8223 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 KMtEL8H~wE2A*Y`J diff --git a/backend/utils/fewshot_selector.py b/backend/utils/fewshot_selector.py index 61afe86..2a77b23 100644 --- a/backend/utils/fewshot_selector.py +++ b/backend/utils/fewshot_selector.py @@ -19,7 +19,7 @@ Few-shot示例选择器 - 基于经验数据集动态选择相关示例 import json import os from pathlib import Path -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Tuple from dataclasses import dataclass import numpy as np import logging @@ -158,6 +158,92 @@ class FewShotSelector: self.cache_path = None logger.info("[OK] Few-shot 使用 Chroma(%s 条)", self._chroma_store.count()) + @property + def is_chroma_backend(self) -> bool: + """是否使用 Chroma 持久化库(`data/embeddings/chroma_fewshot` 等)。""" + return self._chroma_store is not None + + def _passes_filters( + self, + sample: "ExperienceSample", + min_rating: Optional[int], + required_tags: Optional[List[str]], + max_difficulty: str, + exclude_qids: Optional[List[str]], + ) -> bool: + if exclude_qids and sample.qid in exclude_qids: + return False + if min_rating and sample.rating is not None and sample.rating < min_rating: + return False + if max_difficulty == "easy" and sample.difficulty != "easy": + return False + if max_difficulty == "medium" and sample.difficulty == "hard": + return False + if required_tags and not all(tag in sample.tags for tag in required_tags): + return False + return True + + def select_best_with_score( + self, + question: str, + min_rating: Optional[int] = None, + required_tags: Optional[List[str]] = None, + max_difficulty: str = "hard", + exclude_qids: Optional[List[str]] = None, + ) -> Optional[Tuple[ExperienceSample, float]]: + """ + 返回通过筛选的**相似度最高**一条样本及分数 ``[0,1]``(与向量余弦一致:1 - distance)。 + 无命中时返回 ``None``。 + """ + if self._chroma_store is not None: + from utils.fewshot_chroma_store import sample_from_chroma_metadata + + over_fetch = 96 + rows = self._chroma_store.search_raw(question, top_k=over_fetch) + for score, meta, doc in rows: + s = sample_from_chroma_metadata(meta, doc) + if not self._passes_filters( + s, min_rating, required_tags, max_difficulty, exclude_qids + ): + continue + logger.info( + "[Few-shot] best_with_score: qid=%s score=%.4f preview=%r", + s.qid, + float(score), + (s.question_zh or "")[:100], + ) + return (s, float(score)) + return None + + if self._embedder is None or self.embeddings is None or len(self.samples) == 0: + return None + + q_emb = self._embedder.encode( + [question], + batch_size=1, + normalize=True, + show_progress=False, + )[0] + scores = np.dot(self.embeddings, q_emb) + best: Optional[Tuple[float, ExperienceSample]] = None + for idx, (score, sample) in enumerate(zip(scores, self.samples)): + if not self._passes_filters( + sample, min_rating, required_tags, max_difficulty, exclude_qids + ): + continue + s = float(score) + if best is None or s > best[0]: + best = (s, sample) + if best is None: + return None + logger.info( + "[Few-shot] best_with_score(numpy): qid=%s score=%.4f preview=%r", + best[1].qid, + best[0], + (best[1].question_zh or "")[:100], + ) + return (best[1], best[0]) + def _load_samples(self): """从 JSONL 加载样本到内存(非 Chroma 模式必需;Chroma 空库时用于首次灌库)。""" if self.samples_path is None: 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 d33cd2cde2cc90752d8fce7e08ae86ec0ccc3530..824aa08f2a6716a3d9cd6da2b7e9c619c1e0e006 100644 GIT binary patch literal 4000 zcma)9&vO$;7M4c8#`YNh7~3F$H5j8!z+`O#2FoCVA+a?2#yIu z{zl$P3~k&nLZNpzx9?!g4~F>fzxq!`!r}0bq2d384*nF%{G3c}k3AWC8hyI_-O9Hs z;^^7$YtPr8t-o0Q`@}yp|H!=fNSrHpM++k9dZ95H55>%O^pJ=i+KDC}RvuJnXh%my zbd)C}B0ADJd02f=mHmmN;e|%!@Q1N!!;gnj$DdyPuKaEJ`^o1gpPl?6nR^+FB;x;0 z<~Cg!3H1zbmBfVW*+pR%cak|Dq>S*|1AA-FJkLJMzF7O)?Z4jk-Yoo(bafK9BT2!t zU17RANmnK5LlSu?)IZp^(!xsbSObqLjS7ZRqlCe2xEPiiOBkw+svi6SGu8Vt)|ZI; zQJMvNxR1ZwzW3x_+nEv0Oxrmxob#SNC(Jo%P?V2&Y4fHtv}tF)SOodv$VanR$VX8= z;^h?QyM%oCMjraq(6+{fH4d!LqK9&$jE8zCHi|%tjYSOlxO&jX)fMSDyfX-~HpJ4b zi`Y5g%z1WJm|1C1l#h6sWwGmFsMSR+Gzzr!uG-y6pis+JDt6hnbwpT4e7hG)`k{no zU6jlUXV$aN3GHbh9z$Ac7AgaFX0qSLGXiV7$Yz49XKnH@@ z0iZ!3dhv1yXgG+pfkpyu6lg5q4g#V1d0i4H1w;jxhk=d++&EAgh`L5PKu3Y7Yvcsb zu^@61=yW>@i-YMXM5%G0(oEi3bC znsY5D=h>Hpc}W_(n)5L}{tcCEvSm$jb&t2KQOOIuL2tTy>eyAP4~x2$o0s0YF=yh56Fd4+SWy^jw?`G}WmYX8ge zV{FJOpNtTKB^R4T8Kb$faO5lR(OtY2{%cHfBVQ1FogEsGte0zwr1p+ z1%frVEvIEU@;tCIXJWHJFlW8X1z@&koH7T*_KZw#0V#V%c^1OJ%AP6MzY6>^mO+2e z>=pWhqI|^5`>H>x2@<0F^AVkd+Z3;KU%%=OrHY|@Nm$1~I)L<`Ig0d?{m?cDZ8QtC z(a+oWJbPJ~%hI67J_4kh;Jf=L7Nh+}iP4&Z^y(r(uiu|Q`d*}`Sx5SsXRiu#RT}iz zM}WMe(l2s*>$8y1F1A1lmp%KsFt2y`z=FvNt0w&m@UT*^;Ai^pJo}cvq=*kV_ns^2 z0{Mw3R3@O3W*sU^o?RAZSsL`%M}WMhGOcqaZ3ghy|8HKj|GS=jO_Ym80(dh=s;XvRcx7A+NgsajLV8*KVX)2>tOA; z{7|vC6#Ia+l)T}xtSMrZiF`f3c}E<+?Da3mHAQ?HJio)!FgjNarp-)GGZj-@~&dPR&25rO)`6v z*duZIjbh*K<+bTnG|lV^u@iCmPA}KF2=?J(eR1<+k-F|#@B1Z1l$q!_0{(lY_py2R z@#OZYC#Tw}DUq6Lr)FBI8SlVpu*G_D>##_F;tj3M?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 diff --git a/logs/text2sql_api.log b/logs/text2sql_api.log index d59031f..b4d1ded 100644 --- a/logs/text2sql_api.log +++ b/logs/text2sql_api.log @@ -1,67 +1 @@ -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;" +2026-04-16 13:35:24 INFO [utils.repo_logging] repo_logging.py:79 configure_text2sql_api_logging() | 日志文件: C:\Users\24019\Desktop\backman-camel\logs\text2sql_api.log