自然语言 → SQL → 结果 → 图表的对话式取数系统。 手写 LangGraph 状态图编排(禁用
create_react_agent),四层只读安全, 60 条金标评测集 + 自愈专项,FastAPI SSE 流式 + 玻璃拟态三栏前端。
不会 SQL 的业务人员(运营/产品/管理者)用自然语言直接拿数字, 把「提需求 → 数据组排期 → 等回数」的长链路压缩成 30 秒问答。 业务口径(默认 delivered、客户去重用 customer_unique_id、金额含运费)写死在 system prompt,保证人人问出一致口径。
| react_graph(S3) | agentic_graph(S4) | supervisor_graph(S8) | |
|---|---|---|---|
| 编排哲学 | 确定性管线:understand→generate→validate→execute 顺序由代码写死 | LLM 自主编排:模型每轮输出 tool_calls,图只做「执行→观察→回喂」循环 | 规划-分工-裁决:Planner 拆解 → 三个子 Agent(黑板共享、messages 隔离)→ Judge 质量门控 |
| 模型角色 | 只负责 generate 一步 | 自主决定调什么工具、调几轮 | 子 Agent 各司其职;Judge 有否决权(fix 打回重做,revision≤2) |
| 适合场景 | 流程固定、可解释性优先 | 开放任务、工具组合多样 | 高质量要求、可接受额外调用成本 |
| 共同护栏 | MAX_STEPS + TOKEN_BUDGET + 四层安全(与编排无关,双保险) | 同左 | 同左 + judge 两层门控(Python 硬门控 + LLM-as-Judge) |
三者都手写 add_node / add_conditional_edges(禁 prebuilt)。S3/S4 同一套行为用例 8/8 双过。
S8 双跑对照(DeepSeek-V4.1-Flash,2026-10-16):两种编排都 8/8 全过(AGNES 时代 supervisor 只有 7/8,唯一失败来自免费额度 429)。真实代价是 延迟 +56%、成本 +62% (agentic ¥0.0278 / supervisor ¥0.0451)——见下方「计量订正」。
★ 计量订正(面试会问):早期版本写「supervisor token −56%」,那是拿两个口径不一致的 数相除来的。图内
state.token_cost对 agentic 少算 31.4%(47,870 vs API 实测 69,855)—— 根因是 agentic 通过bind_tools暴露的generate_sql工具内部自己也调了一次 LLM (tools/registry.py:111),嵌套调用的 token 没进 state。改用同一口径(直接读 API usage 的CostTracker)重算:−9.8%,不是 −56%。这个不一致是靠"加一个独立计量"才发现的—— 单一计量口径无法自证。详见METRICS.md。 前端「Supervisor 多智能体」按钮可现场演示判官打回循环。
上图:顶部编排切换按钮可见当前处于 Supervisor 多智能体。结论卡里的「数据覆盖完整 / 去重处理 / 结果有效性」三项是子 Agent 分工产出后经判官门控通过的内容——口径自检写进回答本身, 而不是让用户自己去核对(业务取数最怕的是「数字对了但口径错了」)。
| 指标 | 数字 | 说明 |
|---|---|---|
| Few-shot 消融 | 0.600 → 1.000(+0.4) | Schema 注入 vs +Top-3 few-shot,10 条金标 |
| SQL 生成准确率 | 0.860(43/50) | 60 条全量评测(6 条安全题除外),AGNES 基线 |
| 端到端结果匹配 | 0.870(47/54) | 同上 |
| 自愈成功率 | 0.708(17/24) | 注入 3 类真实执行错误后恢复;DeepSeek 复跑同为 0.708 |
| 安全拦截 | 6/6 | 恶意请求全拦,无写操作落地 |
| 跨模型复跑 | SQL 0.780 / 端到端 0.796 | DeepSeek-V4.1-Flash 全量 60 条,实测成本 ¥0.1755、耗时 460s(快 5.8×);差异逐条归因见 METRICS(真实模型差异 0 条) |
| 成本计量单测 | 24/24 | tests/test_cost.py:计价算术 / 缓存命中四种键名 / 峰谷分档 / 熔断;已做变异验证非空转 |
| 攻击用例 | 17/17 | AST 层单测(越权表/子查询绕过/拼接/OUTFILE) |
| 写路径安全用例 | 18/18 | S9 HITL:三道闸 8 + dry-run 零副作用 3 + commit/审计 2 + CSV 导入 5 |
| Supervisor 离线用例 | 5/5 | S8:FakeLLM 驱动全图(happy path / 判官打回循环 / 安全降级 / 规划纠偏) |
| CTE 白名单用例 | 4/4 | S8 连带修复:WITH 别名不被误杀、不掩盖真越权表 |
| MCP 协议用例 | 35/35(RUN_SLOW=1)/ 34 + 1 skip(默认) |
S7:stdio/http 双传输回环 + 安全不旁路 + 第三方工具动态注册 + 子进程凭据白名单 7 条 + 凭据失败形态 11 条(含接线与反向对照) + 启动期黑盒验收 3 条 + 入口不可绕过 2 条 |
| 建库脚本用例 | 11/11 | tests/test_init_db.py:缺 CSV 时点名文件 + 指路 Kaggle(而非裸 FileNotFoundError) |
| 服务端导入用例 | 4/4 | tests/test_server_import.py:服务自己创建数据目录(镜像首次启动曾因 sqlite3.connect() 不建父目录而 OperationalError) |
| 控制台编码闸门 | 6/6 | tests/test_output_encoding.py:AST 扫描 print/raise/logging/exit,禁 emoji 与 ¥(U+00A5)——cp936 编不了,PYTHONIOENCODING=gbk 下会崩 |
| 压测脚本自身用例 | 22/22 | tests/test_bench_concurrency.py:SSE 帧解析(P1 命名事件 / P2 纯 data 两种)、QPS/分位数计算、并发串场判定(压测脚本也要测) |
| 服务层测试 | pytest 174 passed / 2 skipped(默认口径;收集 176)RUN_SLOW=1 + 配好凭据 → 176 passed / 0 skipped |
工具双保险 + 安全层 + S9 写路径 + S8 Supervisor + S7 MCP + 场景包 + 成本计量 + 建库脚本 + 服务端导入 + 编码闸门 + 压测脚本 |
为什么会有 skip、以及为什么这个数必须可复现:只有两条真实 LLM 用例会跳过
(tests/test_tools.py 与 tests/test_mcp.py 各一条),它们由 RUN_SLOW=1 显式开关控制,
而不是「看谁机器上碰巧有 API key」——否则同一个仓库在别人机器上是 174/2、在我这儿是 176/0,
简历写「150 passed」就说不清。现在的口径是:默认(不带任何环境变量、不配任何 key)= 174 passed / 2 skipped,
任何机器一致;要跑全量就 RUN_SLOW=1,且需配好凭据(未配则给一条点名变量的 skip,而不是红)。
其余用例零外部依赖:不联网、不读凭据、不需要人工准备数据。
评测卫生:金标集与 few-shot 库严格错题;金标口径与 system prompt 一致(踩过 3 个评测设计坑)。 性能账本(真实计时/计费,详见 METRICS.md):S5 全量延迟 mean 44.3s / median 37.0s / p95 101s, 自愈的延迟代价 = 一次通过 +25%(50.9s vs 40.7s)。换 DeepSeek-V4.1-Flash 全量复跑: S5 端到端 460s vs AGNES 约 2658s(快 5.8×)、三个评测合计 约 ¥0.29(实测,非估算)。
读路径让 Agent 自主循环;写路径永不进自主循环——模型只有建议权,人有否决权与执行权:
- 写意图(正则粗筛 + LLM 严格 JSON 判定)→ 生成提案 SQL(仅 INSERT/UPDATE/DELETE、表白名单、UPDATE/DELETE 必带 WHERE)
- 事务 dry-run:BEGIN→执行→SELECT changes()→ROLLBACK,零副作用拿到精确影响行数 + 「将被改动的行」取证样本——dry-run 还能提前抓住约束违约(如 NOT NULL),拦截后转读路径兜底
- SSE 推
proposal事件 → 前端审批卡(SQL / 影响行数 / 取证样本 / 风险说明)→ 人点「确认执行 / 拒绝」 /api/confirm复检三道闸 → 独立写凭据(SQLite 去 mode=ro;MySQL 形态 agent_rw 账号)事务执行 →write_audit.jsonl审计留痕
数据接入:左栏「+ 导入CSV」→ 表名/列名清洗 + 类型推断 + 行数质检 → 动态注册进白名单与 Schema 注入(重启自动恢复),Agent 下一句即可查询新表。
上图一次演示里同时可见两个状态:提案未执行(INSERT 卡 + 事务 dry-run 算出的「预估影响 1 行」
- 目标表白名单校验 + 风险说明)与人审通过后(「已执行·实际影响 1 行(审计已记录)」)。 左栏底部的
csv_region_targets就是 CSV 导入后动态注册的新表。
能力开放遵循一条原则:MCP 只是协议外壳,安全语义不变——
- Server(
src/mcp_server.py,FastMCP):把 generate_sql / execute_readonly_sql / compute_metric / render_chart 四个工具按 MCP 暴露,实现层复用src/tools/registry同一份代码(不是绕过安全的副本)。支持 stdio 与 streamable-http 双传输:python src/mcp_server.py --http 8600 - Server 启动期体检:
mcp.run()之前先require_llm_credentials()。 缺凭据默认硬失败(stderr 点名变量 + 三步修法 →exit 1),--allow-degraded则只打 WARN 并不注册 generate_sql(其余 3 个工具不需要 LLM,照常可用)。 ★ stdio 模式下 stdout 是协议信道,所有诊断一律走 stderr——有测试逐行断言 stdout 全是合法 JSON-RPC 帧。 - Client(
src/mcp_client.py):后台事件循环 + AsyncExitStack 保活连接; 任何 MCP server 一行接入:from src.mcp_client import enable_mcp_tools enable_mcp_tools([{"name": "ext", "transport": "http", "url": "http://127.0.0.1:8610/mcp"}]) # 发现 → inputSchema 动态转 Pydantic → register_external_tool → Agent 下一轮可见 # 接本仓库自己的 server 时请显式声明 requires_llm: # 它带启动期体检,缺凭据会 exit(1),客户端闸门必须抢在**连接前**判定,否则只能拿到 # 一条协议层「连接失败」,看不出是凭据问题 enable_mcp_tools([{"name": "p2", "transport": "stdio", "command": sys.executable, "args": ["src/mcp_server.py"], "requires_llm": True}])
- 经 MCP 调
DROP TABLE/ 查白名单外表照样被四层安全拦截; 与原生同名的第三方工具被跳过(原生优先),新工具经 Pydantic 校验通道 dispatch(无旁路)。
子进程凭据:不做全量环境透传,走显式白名单,且取值源是 Settings**(src/mcp_client.py::_MCP_ENV_FROM_SETTINGS)
stdio 子进程的环境不是父进程 os.environ 的复制——MCP SDK 只透传
get_default_environment() 的白名单(本机实测仅 12 个变量:PATH / APPDATA / TEMP /
USERNAME …,零凭据)。于是「父进程明明配好了 DEEPSEEK_API_KEY,子进程却报
Missing credentials」——根因在传参层,不在密钥配置层。
修法刻意不做 **os.environ 全量透传:那等于把父进程所有密钥(数据库口令、云厂商
token、第三方 key)一并塞进一个我们并未逐行审计过的子进程,是用「修好了」换「泄露面无限大」。
正确姿势是显式白名单——子进程要自建 LLM 与只读连库,就只给它这两类凭据;写库口令
agent_rw 刻意不在名单内。
白名单的值一律从 Settings 读,不从 os.environ 读:pydantic-settings 会把 .env
与进程环境合并,只读 os.environ 会漏掉只写在 .env 里的配置(此前 AGNES 能跑纯属
子进程自己也读了 CWD 的 .env 兜底——同一处兜底把同一个缺陷掩盖了两次)。改从 settings 取
还顺带得到两点:子进程不再依赖 CWD 能找到 .env(从任意目录起 mcp_server.py 都能跑);
配置来源唯一,测试可直接 monkeypatch 伪造 settings 字段,不必改真实环境变量,
红绿不再取决于跑测试的机器。
tests/test_mcp.py 有 7 条零 LLM 消耗的用例钉住这条边界(其中test_child_env_takes_value_from_settings_not_environ
用冲突值做决定性断言:os.environ 与 settings 给不同的 key,子进程必须拿到 settings 那份)。
两次变异验证非空转——回退成读 os.environ → 4 红;白名单里塞一个不存在的字段名 → 字段存在性检查当场报错。
凭据缺失怎么失败:三道防线,越早越好
list_tools 本身不调 LLM,所以凭据缺失时连接与注册都会成功,失败曾经是惰性的——
等第一次真正调 generate_sql 才炸,还被上层兜成 degraded。用户看到的是「Agent 好像不太会
写 SQL」,而不是「你少配了一个变量」。现在修成三层:
- ① 服务端启动期(
src/mcp_server.py):mcp.run()之前先require_llm_credentials(), 缺凭据默认exit(1)(stderr 写「缺哪个变量 / 写.env还是setx/ 怎么验证」),--allow-degraded则 WARN + 不注册generate_sql。这一层连客户端都不需要存在。 - ② 客户端闸门(
enable_mcp_tools):看情况分两个时机——spec["requires_llm"]=True时连接前判定(否则子进程先exit(1),我们连list_tools都走不到, 只能等到一条协议层「连接失败」);未声明则连接后按工具集自动判定。 默认strict=True抛MissingCredentialError;strict=False改为打 WARNING 并跳过该 server 的 工具注册——这是「不暴露工具」唯一合法的用法:显式降级、且留了声音。 - ③ 调用期(
_tool_generate_sql):把 SDK 原话(openai.OpenAIError: Missing credentials. Please pass an api_key … or set OPENAI_API_KEY)翻译成{"ok": false, "error_code": "MISSING_CREDENTIALS", "retryable": false, "error": "缺少 LLM 凭据:…DEEPSEEK_API_KEY…"}。 两个理由:SDK 指向的OPENAI_API_KEY我们根本不用,报错必须点名真实变量; ToolMessage 是喂给模型的,retryable: false明确叫它别重试,否则它会当成「工具坏了,换一个试试」白烧步数。
判据按工具集而不是按 server 名:execute_readonly_sql / compute_metric / render_chart
都不建 LLM 客户端,只有 generate_sql 会——否则第三方天气/日历 server 会因为我们自己没配
LLM key 被误拦。这条边界有 17 条零 LLM 用例 + 7 次变异验证守着(去掉结构化错误 → 1 红;闸门失效 → 2 红;
判定一律「需要凭据」→ 2 红;删掉主流程里的闸门调用 → 2 红而 9 条叶子用例全绿;
跳过启动期体检 → 2 红;失败文案打到 stdout → 1 红;白名单漏项 → 夹具阶段红,绝不 skip)。
另有两场景实测:有凭据 33 passed;无凭据 32 passed / 1 skipped(只有真 LLM 那条跳过)。
引擎与业务口径解耦——把「随业务变化的口径级参数」收进一个不可变数据类,换场景不改图/安全/工具一行代码。
| 定义 | src/scenarios/base.py::ScenarioProfile(frozen dataclass,10 个字段) |
| 内置 | src/scenarios/olist.py::OLIST(电商 / SQLite / 数据范围 2016-09~2018-10 / 默认状态 delivered / 去重键 customer_unique_id / 币种 BRL / 业务表枚举) |
| 消费点 | 5 处:agent/nodes.py、agentic_graph.py、supervisor_graph.py、agent/prompting.py、api/server.py |
| 契约测试 | tests/test_scenario.py(6 用例,含医疗场景换装演示:换成 patient 表 + 挂号状态口径,断言 prompt 随之变化) |
字段取舍原则:只收「多个 prompt 共用、且随业务变化」的值;单处使用或纯模板措辞留在各 prompt 里——避免过度抽象。
为什么值得做:口径散落在 5 个 prompt 里时,「改一个业务口径」= 改 5 个地方且必然漏改; 而漏改的后果在评测上是区分度丢失——金标口径与 prompt 口径不一致时,模型「遵守规则」却被判错 (S5 踩过这个坑)。收进 frozen dataclass 后改口径只改一个文件,并由测试钉住 「改 profile → 全部 prompt 同步变化」这条性质。
用户问题
│
▼
FastAPI /api/query (SSE) ┌────────────────────────┐
│ producer thread + queue │ react_graph(确定性管线)│
▼ │ understand → generate │
┌──────────────┐ ┌──────────────────┐ │ → validate → execute │
│ 四层安全层 │───▶│ Olist 9 表 │ │ → self_correct ↺ │
│ 只读/白名单/ │ │ SQLite dev 库 │◀───┤ │
│ LIMIT/超时 │ │ (MySQL 就绪: │ │ agentic_graph(LLM 自主)│
└──────────────┘ │ agent_ro 只读) │ │ agent ⇄ tools 循环 │
└──────────────────┘ │ → respond │
└────────────────────────┘
事件: thought / sql / result / chart / answer / error
▼
三栏前端:Schema 树 │ 对话(SQL 折叠/反馈)│ 结果表 + ECharts
需要准备两样外部资源:① Olist 数据集(9 个 CSV,约 150 MB);② 一个 LLM API Key。 两者都不在仓库里(数据涉版权、key 涉安全),下面给可直接复制执行的步骤。
python -m venv .venv
.venv\Scripts\activate # Windows;Linux/macOS: source .venv/bin/activate
pip install -r requirements.txt数据集:Brazilian E-Commerce Public Dataset by Olist
(Kaggle 免费,需登录账号,下载后得到 archive.zip)。
解压后把内层 9 个 CSV 放到 data/raw/(文件名必须完全一致):
data/raw/
├── olist_customers_dataset.csv
├── olist_geolocation_dataset.csv # 或放原 zip 也行,init_db.py 会自动解压
├── olist_order_items_dataset.csv
├── olist_order_payments_dataset.csv
├── olist_order_reviews_dataset.csv
├── olist_orders_dataset.csv
├── olist_products_dataset.csv
├── olist_sellers_dataset.csv
└── product_category_name_translation.csv
官方
olist_geolocation_dataset.csv有 100 万行 / 58 MB,是最大的一个。 9 个 CSV 必须放全——init_db.py建库前会先跑一次前置检查,缺哪几个就一次列全 (附 Kaggle 链接与data/raw/路径),然后以退出码2结束。 不会「读一张崩一张」,也不会先删旧库再崩。
python scripts/init_db.py
# 建完自动刷新 data/schema.md(给人读的 Schema 文档,随仓库提交)
# 只刷新文档不重建库:python scripts/init_db.py --dump-schema导入 9 表共约 155 万行,首次约 15 秒(实测 13.3s)。脚本会逐表打印行数并与官方口径校验
(对不上会打 data/db/olist.db(约 130 MB,已 gitignore)。
cp .env.example .env# DeepSeek(本项目实测用的就是它)
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=sk-...
LLM_MODEL=deepseek-flash
# 或 Agnes 免费额度:LLM_PROVIDER=agnes / AGNES_API_KEY=...数据库默认落 SQLite(
data/db/olist.db),零配置。切 MySQL 只改DB_URL一行, 见.env.example里的说明(生产形态用agent_ro只读账号)。
python scripts/run_api.py # http://127.0.0.1:8501三栏界面:左 Schema 树 / 中对话(SQL 可折叠、反馈)/ 右结果表 + ECharts。 试一句:「2017年各品类销量 Top5,用图表展示」。
docker compose up -d --wait # api:8501;数据走 ./data 卷,镜像里不带库
curl http://127.0.0.1:8501/api/health
open http://127.0.0.1:8501/ # 三栏界面首次用容器,先让容器把合成库建到卷里(真数据集请按第 2、3 步在宿主机建好再挂进去):
docker compose run --rm --no-deps api python scripts/make_fixture_db.py| 命令 | 作用 |
|---|---|
docker compose up -d --wait |
起 api 并等到 healthcheck 通过才返回 |
docker compose down |
停服务(./data 里的库与审计日志保留) |
docker compose --profile mysql up -d mysql |
额外起 MySQL 8.4(默认 up 不含它) |
一个刻意的设计:compose 里不转发
DB_URL。compose 会自动读项目根的.env做变量插值,而.env里通常写着DB_URL=sqlite:///D:/...——那是宿主机 Windows 路径, 一旦塞进 Linux 容器,症状是「服务起来了、docker ps正常,但一查询就 unable to open database file」。所以让容器按PROJECT_ROOT=/app自己拼默认路径, 要切 MySQL 就在命令行上显式-e DB_URL=...。容器的两个必要条件:
API_HOST=0.0.0.0(run_api.py默认 127.0.0.1 是给本机开发用的, 容器里留着它宿主机就连不上),以及服务自己会mkdir它的数据目录—— 后者是真踩过的坑:server.py导入期就落data/db/service.db,而sqlite3.connect()不会创建缺失的父目录,容器首次启动时data/不存在就OperationalError直接起不来(本地永远复现不了,因为本地data/db/早被真实库占着)。
python -m pytest -q # 默认 174 passed / 2 skipped(收集 176)
python evals/run_eval_s5.py # 60 条全量评测(需 LLM,约 ¥0.18)
python evals/run_selfheal_eval.py # 自愈专项(需 LLM,约 ¥0.04)为什么会有 2 个 skip:只有两条用例真调 LLM,由
RUN_SLOW=1显式控制。 默认口径不联网、不读凭据。但默认口径并非「不需准备数据」——这一点之前写错了,已订正: 有 10 条用例要碰
data/db/olist.db(135 MB,由 Kaggle 数据集建出,.gitignore排除)。 所以准确说法是「任何建过库的机器上 174 / 2」。干净克隆实测是 137 passed / 10 failed / 2 skipped,10 条全因sqlite3.OperationalError。CI 与干净克隆上用合成库补齐(表结构真实、行数合成,够跑通代码路径):
python scripts/make_fixture_db.py # 生成 data/db/olist.db(合成,约 22 MB / 2.4s) python -m pytest -q # → 174 passed / 2 skipped真实业务基数由
init_db.py的EXPECTED行数校验与 METRICS.md 负责,不由单测负责 ——单测该钉不变量,不该钉数据集基数(test_safety.py里写死== 99441的那条已改)。
ci.yml:ruff(真实缺陷类规则)→ 生成合成 fixture 库 → pytest。
docker.yml:--target test 构建 → 容器内 pytest;
以及 docker compose up -d --wait → /api/health → /api/schema(9 张表)→
静态首页 → 容器内只读查一行 → down。两者都是绿的。
为什么 Docker 验证必须放 CI:开发机是 Windows + 没装 Docker (
docker: command not found,wsl.exe也被安全策略拉黑),本地无从验证。
docker.yml首次真跑红过一轮,两个失败同一个根因:server.py导入期落data/db/service.db而目录不存在 → 容器里服务起不来。详见 METRICS.md「T2」。 推 main / PR 触发,约 4 分钟。
为什么要单独生成 fixture 库:库文件不入库,CI 是干净环境。上面那 10 条失败的用例 正是被 CI 的前置侦察(干净克隆)照出来的——CI 的价值在它建立之前就已经兑现过一次。
本地复刻 CI:
python -m venv /tmp/ci && /tmp/ci/bin/pip install -r requirements.txt ruff
/tmp/ci/bin/python scripts/make_fixture_db.py && /tmp/ci/bin/python -m pytest -q别在本地随便跑
make_fixture_db.py:它会写data/db/olist.db, 覆盖掉你的真实库(脚本默认拒绝覆盖,除非加--force)。
| 现象 | 原因 / 处理 |
|---|---|
| 建库报「缺少 N 个数据集文件」(退出码 2) | 第 2 步没做或文件名不对;报错里直接给了 Kaggle 链接和 data/raw/ 路径,缺几个列几个 |
| 启动报「未读取到 DEEPSEEK_API_KEY」(退出码 1) | .env 没配或 LLM_PROVIDER 拼错;报错文案会点名变量 |
行数校验出现 [MISMATCH] |
数据集版本不同或 CSV 被截断,对照官方行数核对 |
脚本输出乱码或 UnicodeEncodeError: 'gbk' codec can't encode |
Windows 控制台是 cp936(GBK)——能编中文,编不了 emoji 和 ¥(U+00A5)。代码里统一用 [OK]/[WARN]/[FAIL] 和 ¥(U+FFE5),tests/test_output_encoding.py 的 AST 闸门守着;若仍撞见,跑该测试会点名到 file:line |
| 429 / 限流 | 免费额度常见;换 DeepSeek 或降低并发 |
详细版(每文件的内容/作用/模块关系/阅读要点 + 整体 pipeline 图 + 术语表)见 ARCHITECTURE.md;下表是速览版。
| 顺序 | 文件 | 知识点 |
|---|---|---|
| 1 | src/core/config.py |
pydantic-settings;SQLite/MySQL URL 抽象(库无关设计) |
| 2 | src/db/connect.py |
SQLAlchemy 双引擎;SQLite mode=ro creator 注入(只读兜底) |
| 3 | src/safety/validator.py |
四层安全:sqlparse AST 白名单 / LIMIT 注入 / progress_handler 超时 |
| 4 | src/agent/state.py |
LangGraph State 定义(Annotated reducer) |
| 5 | src/agent/react_graph.py |
手写确定性状态图(对照 prebuilt 的差异) |
| 6 | src/agent/agentic_graph.py |
手写 ReAct 工具循环:bind_tools → 条件边 → 观察回喂 |
| 7 | src/tools/registry.py |
Function Calling 工具注册:JSON Schema(软约束)+ Pydantic(硬校验)双保险 |
| 8 | src/agent/prompting.py + fewshot.py |
Schema 注入 / few-shot 加权检索(bigram TF,CPU 可部署取舍) |
| 9 | src/api/server.py |
FastAPI + SSE 真流式(producer thread + queue);事件归一化;输出净化 |
| 10 | web/index.html |
原生 JS 三栏布局;SSE 消费;markdown 逐行渲染;ECharts |
| 11 | src/mcp_server.py + src/mcp_client.py |
MCP 双向:FastMCP 工具开放(复用 registry)+ AsyncExitStack 客户端 + 动态注册 |
| 12 | src/scenarios/base.py + olist.py |
场景包:口径级参数收进 frozen dataclass,5 个消费点零改动换业务外壳 |
| 13 | evals/run_eval_s5.py |
评测框架:金标比对 / 断点续跑 / 结果归因 |
| 14 | METRICS.md |
全部指标数字与踩坑记录(面试素材库) |
数据层当前为开发期一次性导入的静态快照;生产落地还需: 企业数据链路(ETL/CDC → 分析从库,Agent 只当读者)、SSO/RBAC 行列权限与审计、 PII 脱敏、schema 变更感知与 schema linking、并发/缓存/限流、Docker 部署。
写路径的「提案-人审-执行」已实现(S9,走 SSE proposal 事件 + /api/confirm 复检三道闸),
剩余的是更成熟的形态:用 LangGraph interrupt 支持可中断/可恢复的长事务审批、
并发场景下的提案状态管理(当前提案活在单次请求的生命周期内)。
详见 METRICS.md 与构建历程图。
已知记账缺陷(未修,已定位):agentic 图通过 bind_tools 暴露的 generate_sql 工具
内部自己也调一次 LLM(tools/registry.py:111),这类嵌套调用的 token 不会进 state.token_cost
(实测少算 31.4%)。当前以 CostTracker(读 API usage)为跨编排比较的权威口径绕开了它;
若要根治,应让工具复用节点实现并把嵌套用量回写 state——同时也能顺带消掉「工具版 SQL 生成
与节点版提示词构建的重复」。
MIT

