From 84bf51c0d0c29657395deb2c49ec3d5ca91713f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Mon, 14 Sep 2026 22:32:26 +0800 Subject: [PATCH] =?UTF-8?q?NL2SQL=EF=BC=9A5=20=E4=BD=8D=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=A2=AB=E4=B8=A2=E5=BC=83=E5=AF=BC=E8=87=B4?= =?UTF-8?q?"=E6=9F=A5=E8=AF=A2=E6=88=90=E5=8A=9F=E4=BD=86=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=94=99"=EF=BC=88=E6=BC=94=E7=A4=BA=E4=BA=A7?= =?UTF-8?q?=E5=93=81=2015911=20=E6=AD=A3=E6=98=AF=205=20=E4=BD=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象 通用自然语言查询里问「查询15911的净值」,返回的是**别的产品**的数据: ``` 问:查询15911的净值 → 返回 511810 货币ETF南方 的净值 问:查询基金代码15911最近30天净值 → 同上 ``` 而且 `status = success`、`message = 查询成功` —— **不报错,只是数据是错的**。 ## 根因 `financial_nl2sql_service._filters()` 只认 6 位数字: ```python re.findall(r"(? tuple[str | None, str | None]: return start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S") +#: 产品代码形态:本平台现库**5 位与 6 位并存**(6 位 25 个,5 位 1 个 —— 演示产品 `15911`)。 +#: +#: 原先只认 `\d{6}`,于是 5 位代码被**静默丢弃**:查询退化成"不带产品过滤", +#: SQL 里没有 `fin_product.product_code = ?`,却仍返回 `status=success` —— 实测 +#: 「查询15911的净值」返回的是 511810 等其它产品的净值,**看起来成功、数据却是错的**。 +_PRODUCT_CODE = re.compile(r"(? list[str]: + codes: list[str] = [] + for match in _PRODUCT_CODE.finditer(question): + code = match.group(0) + if len(code) == 5 and _AMOUNT_UNIT_AFTER.match(question[match.end():]): + continue + codes.append(code) + return sorted(set(codes)) + + def _filters(question: str) -> tuple[dict[str, Any], ...]: return tuple( {"field": "fin_product.product_code", "operator": "=", "value": code} - for code in sorted(set(re.findall(r"(? None: assert result["status"] == "ready" assert "fin_cash_ledger" in result["sql"] assert result["query_plan"]["intent"] == "cash_ledger_query" + + +# --- 产品代码提取:5 位与 6 位并存(2026-09-14 修) -------------------------------- +# +# 原先只认 `\d{6}`,5 位代码(演示产品 `15911`)被静默丢弃 ⇒ 查询**不带产品过滤**, +# 却仍返回 `status=success`:实测问 15911 的净值拿到的是 511810 的数据。 +# 「看起来成功、数据却是错的」比报错更危险,所以这几条要钉住。 + + +@pytest.mark.asyncio +async def test_five_digit_product_code_is_used_as_filter() -> None: + result = await FinancialNL2SQLService().query( + FinancialNL2SQLInput(question="查询15911的净值", dry_run=True), + context(), + ) + + assert result["status"] == "ready" + assert result["parameters"]["filter_0"] == "15911" + # 关键:SQL 里必须**真的带上产品过滤**(列名走别名 `p.`),否则就是"成功但查错数据"。 + assert "p.product_code = :filter_0" in result["sql"] + + +@pytest.mark.asyncio +async def test_six_digit_product_code_still_works() -> None: + result = await FinancialNL2SQLService().query( + FinancialNL2SQLInput(question="查询510300的净值", dry_run=True), + context(), + ) + + assert result["parameters"]["filter_0"] == "510300" + + +@pytest.mark.asyncio +async def test_amount_like_five_digit_number_is_not_treated_as_product_code() -> None: + """「申购金额50000元」里的 50000 是金额,不是产品代码 —— 它后面紧跟单位。""" + result = await FinancialNL2SQLService().query( + FinancialNL2SQLInput(question="查询申购金额50000元的记录", dry_run=True), + context(), + ) + + assert "filter_0" not in result.get("parameters", {}) + + +def test_product_codes_ignores_years_and_short_numbers() -> None: + from app.service.financial_nl2sql_service import _product_codes + + assert _product_codes("查询2026年的净值") == [] # 4 位年份 + assert _product_codes("查询近30天净值") == [] # 天数 + assert _product_codes("查询15911的净值") == ["15911"] + assert _product_codes("对比15911和510300") == ["15911", "510300"]