Experiment 5-10: NL ERP Agent (NL → SQL, Artifact Mode) / 实验 5-10:自然语言交互的 ERP Agent(NL → SQL,artifact 模式)¶
Companion lab for AI Agents in Depth, Chapter 5 — Chinese NL → SQL executed by DB; LLM only produces the SQL artifact, never moves rows itself.
《深入理解 AI Agent》第 5 章:中文自然语言转 SQL 由 DB 执行;LLM 只生成 SQL 制品,不搬运数据。
English¶
Overview¶
Turn Chinese natural-language queries into SQL; the system executes and presents result tables. Core is artifact mode: the Agent only produces the SQL artifact; the database runs the query—LLM never hauls rows—saves tokens, avoids mental arithmetic errors, and still returns large result sets instantly.
Data model (two tables)¶
employees: employee id, name, department, level (higher number = higher rank), hire date, leave date (NULL= active)salaries: employee id, pay date (one row per month,YYYY-MM-01), amount
Data from seed.py with fixed seed 42, relative to “today”—fully reproducible: ~40 employees across 5 depts/levels including some leavers; salaries = base at hire + fixed annual raise per person (unique raises so Q9 ranking is unique); deliberately drop one month’s pay for one active employee (Q10 “arrears”).
10 auto-answered questions¶
- Average tenure per employee
- Active headcount per department
- Department with highest average level
- New hires this year / last year per department
- Dept A average salary from March two years ago through May last year
- Last year, which of depts A/B had higher average salary
- Average salary per level this year
- Average latest-month salary for tenure bands <1y / 1–2y / 2–3y
- Top 10 largest raises last year → this year
- Any unpaid months while employed
Dept A = 研发部 (R&D), B = 销售部 (Sales) (fixed in the prompt).
Run¶
# From the repository root: use the shared Chapter 5 environment
uv sync --locked --python 3.12 --extra ch5
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch5]"
cd chapter5/erp-agent
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
cp env.example .env # OPENAI_API_KEY
python demo.py # same as python demo.py run
OpenRouter fallback: if OPENAI_API_KEY unset, set OPENROUTER_API_KEY (gpt-* → openai/*, else openai/gpt-5.6-luna). Default gpt-5.6-luna is gpt-5.x (org verification on direct OpenAI), so with OPENROUTER_API_KEY OpenRouter is preferred.
demo.py has 4 subcommands (no subcommand = run):
| Subcommand | Needs API | Role |
|---|---|---|
run |
Yes | Online: Agent SQL → execute → compare to reference; per-question print + pass rate |
gold |
No | Offline: built-in gold SQL (gold.py) for all 10; proves data model consistency |
ask |
Yes | One NL query → SQL → execute and print table |
initdb |
No | Create tables and seed a SQLite file for manual sqlite3 inspection |
Common flags: --only 1,5,10, --db erp.db, --model gpt-5.6-luna, --output result.json. Examples:
python demo.py gold # offline 10/10, no API
python demo.py run --only 2,3,6 # Agent SQL for three questions only
python demo.py ask "研发部现在有多少在职员工?"
Online path: in-memory SQLite → seed → per-question Agent SQL → execute → print question / SQL / result / pass → total pass rate.
Correctness checking¶
reference.py is an independent Python reference (no SQL)—computes each answer on seed data. demo.py compares SQL results to reference with multiset + numeric tolerance. gold.py holds 10 hand-written gold SQL statements; python demo.py gold runs them offline without API.
Recent real runs: offline gold 10/10; online run (gpt-5.6-luna) stable 10/10.
Files¶
| File | Role |
|---|---|
demo.py |
CLI (run/gold/ask/initdb): DB, seed, questions, SQL exec, compare, pass rate |
seed.py |
Reproducible seed + schema load |
reference.py |
Independent Python answers for 10 questions |
gold.py |
Hand-written gold SQL (SQLite dialect) for offline gold |
questions.py |
10 NL questions + column/business-hint prompts for the Agent |
agent.py |
NL→SQL Agent (OpenAI SDK; OPENAI_API_KEY or OPENROUTER_API_KEY; default gpt-5.6-luna) |
schema_postgres.sql |
Book’s PostgreSQL DDL (for real Postgres migration) |
About the database¶
This project uses SQLite (zero deps, easy reproduce). The book uses PostgreSQL; SQL is mostly portable. Date differences: here strftime('%Y','now'), julianday(), date('now','-1 year'); on Postgres use EXTRACT(YEAR FROM now()), AGE() / date subtract, now() - interval '1 year', etc.
Notes and caveats¶
- Prompt includes schema-level hints: expected columns/order, business rules (active = empty leave_date; this/last year via
strftime(...,'now',...); A/B dept mapping); no hard-coded years (else “last year / year before” drifts). Hints do not leak answers. - Q8 and Q10 are harder (tenure bands + latest month; recursive months for gaps)—prompt gives recommended SQL structure templates for weaker models.
temperature=0for stability, but LLM is not strictly deterministic; re-run or stronger model (OPENAI_MODEL) if a question flukes.
中文¶
概述¶
把中文自然语言查询自动转成 SQL,由系统执行并直接呈现结果表。核心是 artifact(制品)模式: Agent 只负责「生成 SQL」这个制品,真正的数据查询交给数据库执行,LLM 不亲自搬运数据—— 既省 token、又避免大模型手算出错,几万行结果也能秒回。
数据模型(两张表)¶
employees:员工ID、姓名、部门、级别(数字越大越高)、入职日期、离职日期(NULL = 在职)salaries:员工ID、发薪日期(每月一条,YYYY-MM-01)、工资
数据由 seed.py 用固定随机种子(42)生成、以「今天」为基准相对生成,完全可复现:
约 40 名员工跨 5 个部门/多级别,含若干已离职者;工资按「入职基准 + 每年固定涨薪额」逐月生成,
每人涨薪额互不相同(保证问题 9 排名唯一);并刻意为一名在职员工删掉某月工资(制造问题 10 的「拖欠」)。
10 个自动回答的问题¶
- 平均每个员工在职多久 2. 每个部门有多少在职员工 3. 哪个部门平均级别最高
- 每个部门今年/去年各新入职多少人 5. 前年3月到去年5月 A 部门平均工资
- 去年 A/B 部门平均工资哪个高 7. 今年每个级别平均工资
- 入职一年内 / 一到两年 / 两到三年员工的最近一月平均工资
- 去年到今年涨薪最大的 10 位员工 10. 有没有拖欠工资(某月在职却没发薪)
其中 A 部门 = 研发部,B 部门 = 销售部(在 prompt 中约定)。
运行¶
# 在仓库根目录使用统一的第 5 章环境
uv sync --locked --python 3.12 --extra ch5
# 切换目录前先激活环境:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell:.\.venv\Scripts\Activate.ps1
# Windows cmd:.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch5]"
cd chapter5/erp-agent
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
cp env.example .env # 填入 OPENAI_API_KEY
python demo.py # 等价于 python demo.py run
通用 OpenRouter 兜底:未配置 OPENAI_API_KEY 时,设置 OPENROUTER_API_KEY 即自动
改走 OpenRouter(gpt-* → openai/*,其它 → openai/gpt-5.6-luna)。默认模型
gpt-5.6-luna 属 gpt-5.x,直连 OpenAI 需组织实名认证,故设置了 OPENROUTER_API_KEY
时会优先走 OpenRouter。
demo.py 提供 4 个子命令(不带子命令时等价于 run):
| 子命令 | 是否需要 API | 作用 |
|---|---|---|
run |
需要 | 在线:Agent 生成 SQL → 执行 → 与参考实现比对,逐题打印并给出总通过率 |
gold |
不需要 | 离线自检:执行内置「标准 SQL」(gold.py)跑 10 题并比对,证明数据模型自洽 |
ask |
需要 | 单条自然语言查询 → 生成 SQL → 执行并打印结果表 |
initdb |
不需要 | 建表并把种子数据灌入一个 SQLite 文件,便于用 sqlite3 手工查看 |
常用参数:--only 1,5,10(只跑指定题号)、--db erp.db(用文件库而非内存库)、
--model gpt-5.6-luna(覆盖模型)、--output result.json(导出逐题明细)。示例:
python demo.py gold # 离线跑通 10 题,无需 API
python demo.py run --only 2,3,6 # 只让 Agent 生成这 3 题的 SQL 并校验
python demo.py ask "研发部现在有多少在职员工?"
在线模式会:建 SQLite 内存库 → 灌种子数据 → 逐题让 Agent 生成 SQL → 执行 → 打印 「问题 / 生成的 SQL / 查询结果 / 是否通过」,最后给出总通过率。
正确性校验¶
reference.py 是独立的 Python 参考实现:不走 SQL,直接在种子数据上把每题答案算一遍。
demo.py 把 SQL 的执行结果与参考答案按「多重集合 + 数值容差」比对,逐题打印 通过/不通过。
gold.py 是人工编写的 10 条「标准 SQL」,python demo.py gold 离线执行它们即可自检,无需 API。
最近一次真实运行:离线 gold 通过率 10/10;在线 run(gpt-5.6-luna)
全部 10 题稳定通过,总通过率 10/10。
文件¶
| 文件 | 作用 |
|---|---|
demo.py |
命令行入口(run/gold/ask/initdb):建库、灌数据、跑题、执行 SQL、比对、打印通过率 |
seed.py |
可复现的种子数据生成 + 建表灌数 |
reference.py |
10 题的独立 Python 参考实现(校验基准) |
gold.py |
10 题人工编写的「标准 SQL」(SQLite 方言),供 gold 离线自检 |
questions.py |
10 个自然语言问题 + 给 Agent 的「返回列/业务口径」提示 |
agent.py |
NL→SQL Agent(OpenAI SDK,读 OPENAI_API_KEY 或 OPENROUTER_API_KEY 兜底,默认 gpt-5.6-luna) |
schema_postgres.sql |
书中 PostgreSQL 版建表 DDL(迁移到真实 Postgres 时参考) |
关于数据库¶
本项目用 SQLite(零依赖、可直接复现)。书中示例用 PostgreSQL,SQL 大体通用,
差异主要在日期函数:本项目用 SQLite 的 strftime('%Y','now')、julianday()、date('now','-1 year') 等;
迁到 PostgreSQL 时对应换成 EXTRACT(YEAR FROM now())、AGE()/日期相减、now() - interval '1 year' 等即可。
说明与注意事项¶
- Agent 的 prompt 里补充了 schema 级提示:期望返回哪些列/顺序、业务口径(在职=leave_date 为空、
今年/去年如何用
strftime(...,'now',...)推导、A/B 部门映射),以及禁止硬编码年份 (否则模型不知道「今天」是哪年,会把「前年/去年」猜错)。这些是合理的 schema 提示,不泄露具体答案。 - 问题 8、10 较复杂(工龄分档取最近一月工资、递归生成在职月份找空缺),在提示里给了推荐的 SQL 结构模板,帮助较小模型稳定产出正确 SQL。
temperature=0让输出尽量稳定,但 LLM 仍非严格确定性;若个别题偶发偏差,重跑即可, 也可换更强的模型(设OPENAI_MODEL)。
Notes / 说明¶
- Prefer
python demo.py goldwithout a key. / 无 Key 优先python demo.py gold。 - Commands/code/paths/env vars are identical in both language sections. / 命令、代码、路径与环境变量在中英文两侧保持一致。