Files
group_xinghuo_jinrong/docs/项目框架设计/数据分析Agent-代码迭代.md
T

146 lines
9.2 KiB
Markdown
Raw Normal View History

# 数据分析 Agent · 代码迭代文档
> 演化记录:从空壳到可用的 NL2SQL Agent,含模块实现、测试基线、问题修复与待办。
> 关联:`docs/memory/FRAMEWORK.md`(技术选型 / 实现状态)、`docs/需求拆解/01-数据分析Agent需求规格.md`(需求 D-xx)、`docs/项目框架设计/数据分析Agent架构说明书.md`。
## 1. 定位与范围
数据分析 Agent(`AnalystAgent`)把**自然语言问数**翻译为一条只读 SELECT 并执行,面对 `jinrong_core`(模拟 Core 库,L0 事实)与部分 `jinrong_agent` 表,最终输出「人话解读 + 数据表格 + 元信息」。当前为 **analyst 角色全量(full 域)可跑**的实现。
约束(不可越界):仅 SELECT 只读 · 不带处置(不写预警)· 数字不可编造 · 留痕审计。
## 2. 代码演进时间线
| 日期 | 里程碑 | Git | 说明 |
| --- | --- | --- | ---- |
| 2026-09-05 | 项目脚手架 + 需求/框架定稿 | `0374010` `1ddd44a` | FastAPI 分层 / 双库 / LangGraph 选型;分析 Agent 仅空壳 |
| 2026-09-09 | **分析 Agent 全套落地** | `b19a241` | api/service/schema/校验/护栏/缓存/字典/LLM + tests + 文档,41 文件 +3463 行 |
| 2026-09-10 | 三问查数 + **护栏 Decimal/标识误报修复** + 流水消歧 | 工作区 | guardrail.py(Decimal/编号/日期时间/左边界)、dict_service(流水多义),degrade→success/clarify |
| 2026-09-10 | 20 题实测 + **数字护栏误拦截修复** | 工作区 | guardrail.py 两处修复,4 题 degrade → success |
核心逻辑类:`app/service/analyst_agent.py`(`AnalystAgent.run()` 7 步编排,兼容 `build_graph()` LangGraph 适配)。
## 3. 模块实现清单
| 模块 | 文件 | 职责 | 状态 | 本轮变化 |
| --- | --- | --- | --- | --- |
| 编排 | `service/analyst_agent.py` | 消歧→SQL→校验→执行→解读→护栏→留痕 | 已实现 | – |
| 路由 | `api/analyst.py` | `/chat` `/dashboard` `/assets` `/ops/metrics` | 已实现 | – |
| 鉴权 | `utils/auth.py` + `api/deps.py` | JWT(HS256) + 角色→数据域 RBAC | 已实现 | – |
| 数据访问 | `service/analytics_repo.py` | 只读执行 / 归属白名单 / 留痕 / 资产沉淀 / 空态三分类 | 已实现 | – |
| SQL 校验 | `service/sql_guard.py` | 五层只读校验(关键字 / 表白名单 / 多语句 / 行级归属 / 粒度) | 已实现 | – |
| 数字护栏 | `service/guardrail.py` | 解读数字与结果逐字校验(D-10) | 已实现 | **本轮修复** |
| 指标口径 | `service/dict_service.py` | 指标字典 + 多义词消歧(N-01) | 已实现 | – |
| Schema 提示 | `service/schema_meta.py` | 注入表/列说明给 LLM | 已实现 | – |
| LLM | `service/llm.py` | DeepSeek 对话 / 提取 SQL / 成本估算 | 已实现 | – |
| 查询缓存 | `service/cache_service.py` | Redis→内存降级;权限指纹键;TTL 分层(D-06) | 已实现 | – |
| 未接入 | `rag_service.py` `memory_service.py` | RAG 知识库 / L1-L3 业务记忆 | 空壳存根 | – |
### 请求流转(`AnalystAgent.run()`)
```
auth 域判定 → 指标消歧(若多义→clarify) → 生成SQL →
SQL 五层校验(失败→deny) → 执行(缓存优先) → 空态三分类 →
LLM 解读 → 数字护栏(失败重试1次→降级) → 组装响应 → 留痕 double-write
```
## 4. 需求覆盖(对照需求规格)
| 需求 | 能力 | 落点 |
| --- | --- | --- |
| D-01/02 | NL→SQL、只读查询 | `_generate_sql` + `validate` |
| D-04 | 查询/审计留痕 | `log_query` + `log_audit` |
| D-06 | 结果/模板缓存 | `cache_service` |
| D-07 / N-01 | 口径字典 / 消歧 | `dict_service` |
| D-10 | 数字护栏 | `guardrail`(本轮加强) |
| D-11 | 资产沉淀 | `assets` + `insert_asset` |
| D-12 / N-08 | 看数板 / 运营指标 | `dashboard` / `ops_metrics` |
| RBAC | 角色→数据域 | `auth.assert_analyst_access` |
## 5. 质量基线
**单元测试:** `tests/test_*.py` 覆盖 agent / api / cache / dict / guardrail(14) / sql_guard / llm / repo。改为本机确认 `tests/test_guardrail.py` OK。
**实测基线(2026-09-10,analyst 全量域,20 题):**
| 维度 | 结果 |
| --- | --- |
| 成功返回可用结果 | 18 / 20(90%) |
| 澄清后继续(多义) | 1(「规模」持仓/产品多义,属预期) |
| 白名单误杀 deny | 1(Q17 触发 `create` 危险词误判) |
| 数字护栏误拦截(修复前) | 4(Q1/Q9/Q19/Q20)→ 修复后可全过 |
| 内容正确性 | 17 / 18 数字无误写;1 题口径跑题(Q7) |
## 6. 本轮迭代详情(2026-09-10)
### 背景
用户问「用户持仓前三? 申购前三? 交易流水前三?」验证 NL2SQL 查数。端到端跑通后暴露两个问题:数字护栏把正确回答误判为降级,「交易流水前三」语义漂移。
### 问题 1:数字护栏误拦截(三类根因)
- **Decimal 不被识别**:pymysql 对 `DECIMAL`/`SUM(...)` 返回 `Decimal`,`_to_float` 只认 `int/float/str`,金额被当 `None` 丢弃,`result_numbers()` 只剩 `{0.0, 行数}`,导致「315.00万元」这类正确解读被判失败,三次查询全部 `degrade`。
- **ID/名称/日期里的数字误报**:`extract_numbers` 把 `CUST-3001` 拆成 `-3001`、把「沪深300指数」拆出 `300`、把「9月3日10:00」拆出 `9`,从而误判「说错数字」。
- 其中「中文后直接接编号」(客户CUST-3001、产品PROD-510300)会使 `\b` 边界失效(中文也属 `\w`)。
### 修复 1(`app/service/guardrail.py`)
1. `_to_float` 新增 `Decimal` 分支。
2. `extract_numbers` 先屏蔽编号 token(`CUST-/TRD-/PROD-/STAFF-`,不加 `\b`)与日期/时间 token(`YYYY-MM-DD`、`M月D日`、`HH:MM`),再匹配。
3. `NUMBER_RE` 加左边界 `(?<![\w])`,挡掉「沪深300」「中证500」等名称内数字。
### 问题 2:「交易流水前三」语义漂移
同一问题多次运行,LLM 分别给出「按时间取最新 3 笔」「按交易笔数排序」「按金额排序」三种口径,非确定性(「流水」无口径定义,`_detect_ambiguity` 拦不住)。
### 修复 2(`app/service/dict_service.py`,N-01 / B2)
新增 `交易流水金额` 与 `交易流水笔数` 两个口径,别名均含「交易流水/流水」,「流水」成为多义词 → `resolve()` 返回 `Ambiguity`,Agent 反问「按金额还是按笔数」;「流水金额/流水笔数」仍各指向唯一口径。
### 验证
- 单测:`test_guardrail.py` 14 项(新增 7 项回归)、`test_dict_service.py` 7 项(新增 4 项)全 OK;`sql_guard`/`cache_service`/`agent` 编排无回归(合计 42 项)。
- 端到端(进程内 + HTTP 8002):`用户持仓前三`、`申购前三` 由 `degrade → success`,数字逐一正确;`交易流水前三` 由漂移 → `clarify`(反问金额/笔数)。
> 踩坑备注:① 8000 端口跑的是 `customer-service-agent` 分支(auth/chat),非数据分析 Agent,勿按端口混淆分支;② 旧服务由 node 守护进程自动重启,非管理员 shell 杀 python 无效。
## 7. 上一轮迭代详情(2026-09-10)
### 问题:数字护栏误拦截(4 题 degrade)
- **Q1/Q20** 根因:结果列是**比率**(`0.1515`),解读写**百分比**(`15.15%`),护栏未做「比率 ×100」换算即判错。
- **Q19** 根因:解读「近 30 天」的时间窗数字 `30` 被正则当成数据数字,与结果 `0` 比对失败。
- 附带:Q9 依赖 LLM 措辞(是否用「万」)存在波动,与护栏策略耦合。
### 修复(`app/service/guardrail.py`)
1. 新增 `_WINDOW_TOKEN`:数字提取前遮挡「近30天/近 30 月」等时间窗口径,防窗口数字污染。
2. `check_numbers()`:当结果集合含 0~1 的比率时,追加 `0.01` 比对刻度(识别 `n×0.01 ≈ 结果`,即百分比=比率×100)。
### 验证
- 离线确定性用例:Q1/Q19/Q20 通过;编造数字负例(`999999.99`)仍被拦截(护栏未削弱)。
- 既有 14 项 guardrail 单测均 OK,无回归。
- 端到端真实接口(重启后端加载新码后):Q1/Q9/Q19/Q20 全部 `degrade → success`,数字逐一核对正确。
> 踩坑备注:后端 `--reload` 对已改模块未实时生效,须重启进程验证代码变更。
## 8. 已知问题与下一步
| # | 问题 | 影响 | 建议 |
| --- | --- | --- | --- |
| 1 | Q17:SQL 白名单把 `create` 当危险词,字段/列名命中即 deny | 一次误拦 | `create` 仅拦截语句级(`CREATE TABLE`),字段/列名放行 |
| 2 | Q7:库无「城市」字段时,Agent 静默改按「职业」统计而非澄清/拒绝 | 答非所问 | NL2SQL 对库结构不匹配加「澄清/拒绝」兜底 |
| 3 | 数字护栏对"聚合比率/多位小数"依赖 LLM 措辞 | flaky | 可考虑允许集合追加百分比别名校验以进一步收敛 |
| 4 | guardrail 修复尚无回归用例 | 防复发缺口 | 将 4 题误报场景补入 `tests/test_guardrail.py` |
| 5 | RAG / 业务记忆仍为空壳 | 能力边界 | 待后续迭代按架构说明书接入 |
## 9. 验证方式
```text
运行:uvicorn app.main:app --reload → POST /api/analyst/chat(Bearer 开发 token)
探活:GET /health
单测:tests/(guardrail、sql_guard、agent、api 等)
实测:scripts/dev/run_query_battery.py(20 题基线)
配置:.env(DEEPSEEK_API_KEY 必填;缺失则 LLM 报错)
```