ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.env
__pycache__/
*.pyc
*.db
+232
View File
@@ -0,0 +1,232 @@
# Experiment 5-10: NL ERP Agent (NL → SQL, Artifact Mode) / 实验 5-10:自然语言交互的 ERP AgentNL → SQLartifact 模式)
> 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 制品,不搬运数据。
← [Chapter 5 index / 返回第 5 章目录](../README.md)
---
## 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 months pay for one active employee (Q10 “arrears”).
### 10 auto-answered questions
1. Average tenure per employee
2. Active headcount per department
3. Department with highest average level
4. New hires this year / last year per department
5. Dept A average salary from March two years ago through May last year
6. Last year, which of depts A/B had higher average salary
7. Average salary per level this year
8. Average latest-month salary for tenure bands &lt;1y / 12y / 23y
9. Top 10 largest raises last year → this year
10. Any unpaid months while employed
Dept A = 研发部 (R&D), B = 销售部 (Sales) (fixed in the prompt).
### Run
```bash
# 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:
```bash
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` | Books 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=0` for 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 个自动回答的问题
1. 平均每个员工在职多久 2. 每个部门有多少在职员工 3. 哪个部门平均级别最高
4. 每个部门今年/去年各新入职多少人 5. 前年3月到去年5月 A 部门平均工资
6. 去年 A/B 部门平均工资哪个高 7. 今年每个级别平均工资
8. 入职一年内 / 一到两年 / 两到三年员工的最近一月平均工资
9. 去年到今年涨薪最大的 10 位员工 10. 有没有拖欠工资(某月在职却没发薪)
其中 A 部门 = 研发部,B 部门 = 销售部(在 prompt 中约定)。
### 运行
```bash
# 在仓库根目录使用统一的第 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`(导出逐题明细)。示例:
```bash
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 AgentOpenAI 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 gold` without a key. / 无 Key 优先 `python demo.py gold`
- Commands/code/paths/env vars are identical in both language sections. / 命令、代码、路径与环境变量在中英文两侧保持一致。
+114
View File
@@ -0,0 +1,114 @@
"""
NL -> SQL Agentartifact 模式)。
Agent 只负责「生成 SQL 制品」,不亲自搬运数据:
真正的数据查询由系统(demo.py)用生成的 SQL 在 SQLite 上执行,结果表直接呈现。
"""
import os
import re
from datetime import date
from openai import OpenAI
MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.6-luna")
# --- 通用 OpenRouter 兜底 ---
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def _map_to_openrouter_model(model: str) -> str:
"""把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。"""
if not model or "/" in model:
return model or "openai/gpt-5.6-luna"
m = model.lower()
if m.startswith(("gpt-", "o1", "o3", "o4")):
return "openai/" + model
if m.startswith("claude"):
if "haiku" in m:
return "anthropic/claude-haiku-4.5"
if "sonnet" in m:
return "anthropic/claude-sonnet-4.6"
return "anthropic/claude-opus-4.8"
if m.startswith("gemini"):
return "google/" + model
return "openai/gpt-5.6-luna"
def _make_client_and_model(model: str):
"""构造客户端并解析模型名,含通用 OpenRouter 兜底。返回 (client, resolved_model)。
- 有 OPENAI_API_KEY:直连;但 model 为 gpt-5.x 且同时设置了 OPENROUTER_API_KEY
时优先走 OpenRouter(直连 gpt-5.6 需组织实名认证)。
- 无 OPENAI_API_KEY 但有 OPENROUTER_API_KEY:改走 OpenRouter(模型名自动映射)。
"""
api_key = os.environ.get("OPENAI_API_KEY")
base_url = os.environ.get("OPENAI_BASE_URL")
orkey = os.environ.get("OPENROUTER_API_KEY")
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
if prefer_or or (not api_key and orkey):
api_key, base_url, model = orkey, OPENROUTER_BASE_URL, _map_to_openrouter_model(model)
kw = {}
if api_key:
kw["api_key"] = api_key
if base_url:
kw["base_url"] = base_url
return OpenAI(**kw), model
SYSTEM_PROMPT = """你是一个「自然语言转 SQL」的 ERP 数据助手。
用户给你一个中文问题,你只输出一条可直接执行的 **SQLite** SQL 查询,不要任何解释、不要 markdown 代码块。
今天的日期是 {today}。但**严禁在 SQL 里硬编码年份数字**(如 '2024''2022-01-01'),
一律用 strftime(...,'now',...) 从数据库当前日期推导,避免年份猜错。
数据库 schemaSQLite):
employees(emp_id INTEGER 主键, name 姓名, department 部门, level 级别[数字越大越高],
hire_date 入职日期'YYYY-MM-DD', leave_date 离职日期'YYYY-MM-DD'NULL 表示在职)
salaries(emp_id, pay_date 发薪日期'YYYY-MM-01'[每月一条], salary 当月工资)
salaries.emp_id 关联 employees.emp_id。
业务与方言约定:
- 「今年」= strftime('%Y','now'),「去年」= strftime('%Y','now','-1 year')
「前年」= strftime('%Y','now','-2 years')。
- 计算「今天」请用 date('now')(不要带时间部分);两个日期相差天数用
julianday(date('now')) - julianday(hire_date)。
- 「A部门」= 研发部,「B部门」= 销售部。
- 「在职」指 leave_date IS NULL。
- 发薪月份可用 strftime('%Y-%m', pay_date) 得到 'YYYY-MM'
- 只输出一条 SELECT(可含 WITH/CTE),不要写多条语句或 DDL/DML。
严格按用户附带的「返回列」要求组织 SELECT 的列与顺序。
"""
class SQLAgent:
def __init__(self, model: str = MODEL):
self.client, self.model = _make_client_and_model(model)
def generate_sql(self, nl_question: str, hint: str) -> str:
user = f"问题:{nl_question}\n要求:{hint}\n请只输出一条 SQLite SQL。"
# 推理模型(gpt-5 / o 系列等)不接受 temperature=0。
_reasoning = any(k in (self.model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
resp = self.client.chat.completions.create(
model=self.model,
temperature=1 if _reasoning else 0,
messages=[
{"role": "system",
"content": SYSTEM_PROMPT.format(today=date.today().isoformat())},
{"role": "user", "content": user},
],
)
return _clean_sql(resp.choices[0].message.content)
def _clean_sql(text: str) -> str:
"""去掉 markdown 代码块围栏等杂质,只留 SQL。"""
text = text.strip()
# 去掉 ```sql ... ``` 或 ``` ... ```
fence = re.match(r"^```(?:sql)?\s*(.*?)\s*```$", text, re.DOTALL | re.IGNORECASE)
if fence:
text = fence.group(1).strip()
# 去掉可能残留的前缀反引号
text = text.strip("`").strip()
return text
+406
View File
@@ -0,0 +1,406 @@
#!/usr/bin/env python3
"""Canonical PostgreSQL + live-model campaign for Experiment 5-10."""
from __future__ import annotations
import argparse
import datetime as dt
import decimal
import hashlib
import html
import json
import os
import re
import shutil
import time
from pathlib import Path
from typing import Any
import psycopg2
from openai import OpenAI
from playwright.sync_api import sync_playwright
import reference
import seed
from questions import QUESTIONS
HERE = Path(__file__).resolve().parent
POSTGRES_DDL = (HERE / "schema_postgres.sql").read_text(encoding="utf-8")
PG_HINTS = {
1: "Use COALESCE(leave_date, CURRENT_DATE) - hire_date to obtain integer days, then AVG. Return one numeric column.",
2: "Active means leave_date IS NULL. GROUP BY department. Return department and active count.",
3: "Average level across all employees by department; ORDER BY the average descending and LIMIT 1. Return department only.",
4: "Use COUNT(*) FILTER with EXTRACT(YEAR FROM hire_date) for current and previous years. Return department, this-year count, last-year count; omit departments with both zero.",
5: "A=研发部. Inclusive dates are March 1 two years ago through May 31 last year; derive years from CURRENT_DATE with make_date, never literals. Return AVG(salary).",
6: "A=研发部 and B=销售部. Join employees to salaries; filter pay_date to previous calendar year, group by department, and return department plus average salary for exactly those two departments.",
7: "Join salary rows to employees, filter pay_date to current calendar year, group by level. Return level and average salary.",
8: "First select each employee's latest salary with DISTINCT ON (emp_id) ordered by pay_date DESC. Bucket CURRENT_DATE-hire_date as <365 入职一年内, 365..729 一到两年, 730..1094 两到三年; exclude older. Return bucket and average latest salary.",
9: "Aggregate each employee's average salary separately for current and previous calendar years with FILTER, keep employees having both, compute current minus previous, order descending, LIMIT 10. Return name and raise amount.",
10: "For every employee generate each employed month with LATERAL generate_series(date_trunc('month', hire_date), date_trunc('month', COALESCE(leave_date,CURRENT_DATE)), interval '1 month'); left join salaries by emp_id and month. Return missing emp_id and to_char(month,'YYYY-MM').",
}
SYSTEM = """You are an ERP natural-language-to-SQL Agent. Output exactly one read-only
PostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.
Schema:
employees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,
hire_date DATE, leave_date DATE NULL)
salaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,
PRIMARY KEY(emp_id,pay_date))
Business meanings: leave_date NULL means active; A department is 研发部; B is 销售部.
Use CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the
requested output columns exactly. You write only the SQL artifact: you do not see,
copy, summarize, or calculate over result rows."""
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def jsonable(value: Any) -> Any:
if isinstance(value, (dt.date, dt.datetime)):
return value.isoformat()
if isinstance(value, decimal.Decimal):
return float(value)
if isinstance(value, dict):
return {str(key): jsonable(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [jsonable(item) for item in value]
return value
def atomic_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
json.dumps(jsonable(value), ensure_ascii=False, indent=2), encoding="utf-8"
)
temporary.replace(path)
def backend(provider: str, model: str | None) -> tuple[OpenAI, str, str]:
choices = {
"ark": (os.getenv("ARK_API_KEY"), "https://ark.cn-beijing.volces.com/api/v3", model or "doubao-seed-1-6-250615"),
"moonshot": (os.getenv("MOONSHOT_API_KEY") or os.getenv("KIMI_API_KEY"), "https://api.moonshot.cn/v1", model or "kimi-k3"),
"openrouter": (os.getenv("OPENROUTER_API_KEY"), "https://openrouter.ai/api/v1", model or "openai/gpt-5.6-luna"),
"openai": (os.getenv("OPENAI_API_KEY"), os.getenv("OPENAI_BASE_URL"), model or "gpt-5.6-luna"),
}
key, base_url, resolved = choices[provider]
if not key:
raise RuntimeError(f"provider={provider} has no configured credential")
kwargs: dict[str, Any] = {"api_key": key, "timeout": 180.0, "max_retries": 4}
if base_url:
kwargs["base_url"] = base_url
return OpenAI(**kwargs), resolved, base_url or "https://api.openai.com/v1"
def clean_sql(text: str) -> str:
text = text.strip()
match = re.match(r"^```(?:sql)?\s*(.*?)\s*```$", text, re.I | re.S)
if match:
text = match.group(1).strip()
text = text.strip("`").strip()
if text.endswith(";"):
text = text[:-1].rstrip()
if not re.match(r"^(SELECT|WITH)\b", text, re.I):
raise ValueError("model did not return a SELECT/WITH artifact")
if ";" in text or re.search(r"\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|COPY|CALL|DO)\b", text, re.I):
raise ValueError("non-read-only or multiple-statement SQL rejected")
return text
def normalize(rows: list[tuple[Any, ...]]) -> list[tuple[tuple[str, Any], ...]]:
normalized = []
for row in rows:
values = []
for value in row:
if isinstance(value, (int, float, decimal.Decimal)) and not isinstance(value, bool):
values.append(("n", round(float(value), 2)))
else:
values.append(("s", str(value).strip()))
normalized.append(tuple(values))
return normalized
def equal_rows(expected: list[tuple[Any, ...]], actual: list[tuple[Any, ...]], tolerance: float = 0.1) -> tuple[bool, str]:
remaining = list(normalize(actual))
wanted = normalize(expected)
if len(wanted) != len(remaining):
return False, f"row count expected={len(wanted)} actual={len(remaining)}"
for expected_row in wanted:
for index, actual_row in enumerate(remaining):
if len(expected_row) != len(actual_row):
continue
matches = all(
a[0] == b[0]
and (abs(a[1] - b[1]) <= tolerance if a[0] == "n" else a[1] == b[1])
for a, b in zip(expected_row, actual_row)
)
if matches:
remaining.pop(index)
break
else:
return False, f"missing expected row {expected_row}"
return True, "independent Python reference matched"
def create_schema(connection, schema: str, employees: list[dict[str, Any]], salaries: list[dict[str, Any]]) -> str:
with connection.cursor() as cursor:
cursor.execute(f'CREATE SCHEMA "{schema}"')
cursor.execute(f'SET search_path TO "{schema}"')
cursor.execute(
"""CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY, name TEXT NOT NULL, department TEXT NOT NULL,
level INTEGER NOT NULL, hire_date DATE NOT NULL, leave_date DATE)
"""
)
cursor.execute(
"""CREATE TABLE salaries (
emp_id INTEGER NOT NULL REFERENCES employees(emp_id), pay_date DATE NOT NULL,
salary INTEGER NOT NULL, PRIMARY KEY(emp_id,pay_date))
"""
)
cursor.executemany(
"INSERT INTO employees VALUES (%s,%s,%s,%s,%s,%s)",
[(e["emp_id"], e["name"], e["department"], e["level"], e["hire_date"], e["leave_date"]) for e in employees],
)
cursor.executemany(
"INSERT INTO salaries VALUES (%s,%s,%s)",
[(s["emp_id"], s["pay_date"], s["salary"]) for s in salaries],
)
cursor.execute("SELECT version()")
version = cursor.fetchone()[0]
connection.commit()
return version
def generate_sql(
client: OpenAI,
model: str,
question: dict[str, Any],
*,
execution_feedback: str | None = None,
prior_sql: str | None = None,
attempt: int = 1,
) -> tuple[str, dict[str, Any]]:
feedback = ""
if execution_feedback:
feedback = (
"\nThe prior SQL failed PostgreSQL execution. Repair only that executable error; "
"no database rows or reference answer are available to you.\n"
f"Prior SQL:\n{prior_sql}\nPostgreSQL error:\n{execution_feedback}"
)
messages = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"Question: {question['nl']}\nPostgreSQL guidance: {PG_HINTS[question['id']]}{feedback}"},
]
request: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": 1 if any(x in model.casefold() for x in ("kimi-k3", "gpt-5", "o1", "o3", "o4")) else 0,
}
started = time.monotonic()
response = client.chat.completions.create(**request)
choice = response.choices[0]
if choice.finish_reason == "length":
raise RuntimeError("truncated SQL response")
sql = clean_sql(choice.message.content or "")
usage = response.usage
receipt = {
"question_id": question["id"],
"purpose": "initial_sql_generation" if attempt == 1 else "postgres_execution_error_repair",
"attempt": attempt,
"called_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
"latency_s": round(time.monotonic() - started, 3),
"request": request,
"response": {"id": response.id, "model": response.model, "finish_reason": choice.finish_reason, "content": choice.message.content},
"usage": {
"prompt_tokens": getattr(usage, "prompt_tokens", None),
"completion_tokens": getattr(usage, "completion_tokens", None),
"total_tokens": getattr(usage, "total_tokens", None),
"cached_prompt_tokens": getattr(getattr(usage, "prompt_tokens_details", None), "cached_tokens", None),
},
}
if not response.id or not receipt["usage"]["total_tokens"]:
raise RuntimeError("provider omitted receipt metadata")
return sql, receipt
def render_results(run_dir: Path, records: list[dict[str, Any]]) -> dict[str, Any]:
sections = []
for record in records:
rows = record.get("rows") or []
table = "<p>(no rows)</p>" if not rows else (
"<table>" + "".join(
"<tr>" + "".join(f"<td>{html.escape(str(cell))}</td>" for cell in row) + "</tr>"
for row in rows
) + "</table>"
)
sections.append(
f"<section><h2>{record['id']}. {html.escape(record['question'])}</h2>"
f"<pre>{html.escape(record['sql'])}</pre>{table}"
f"<p class={'ok' if record['passed'] else 'bad'}>{'PASS' if record['passed'] else 'FAIL'}: {html.escape(record['comparison'])}</p></section>"
)
document = """<!doctype html><meta charset=utf-8><title>Experiment 5-10 PostgreSQL artifacts</title>
<style>body{font-family:system-ui;margin:30px;background:#f7f8fa;color:#172033}section{background:white;padding:18px;margin:16px 0;border-radius:12px}table{border-collapse:collapse}td{border:1px solid #ccd3dd;padding:5px 9px}pre{white-space:pre-wrap;background:#eef2f7;padding:12px}.ok{color:#08783e}.bad{color:#b42318}</style>
<h1>ERP Agent: SQL artifacts executed by PostgreSQL</h1>""" + "".join(sections)
html_path = run_dir / "results.html"
html_path.write_text(document, encoding="utf-8")
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1400, "height": 1000})
page.set_content(document, wait_until="load")
screenshot = run_dir / "results.png"
page.screenshot(path=str(screenshot), full_page=True)
version = browser.version
browser.close()
return {"browser": "Chromium", "version": version, "html": "results.html", "screenshot": "results.png"}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--provider", choices=["ark", "moonshot", "openrouter", "openai"], default="ark")
parser.add_argument("--model", default=None)
parser.add_argument("--postgres-dsn", default=os.getenv("CH5_ERP_POSTGRES_DSN", "dbname=postgres"))
parser.add_argument("--run-id", default=None)
args = parser.parse_args()
started = dt.datetime.now(dt.timezone.utc)
run_id = args.run_id or started.strftime("%Y%m%dT%H%M%SZ-5_10-postgresql")
run_dir = HERE / "validation" / "runs" / run_id
if run_dir.exists():
raise FileExistsError(f"immutable run exists: {run_dir}")
run_dir.mkdir(parents=True)
schema = "exp5_10_" + re.sub(r"[^0-9A-Za-z]", "", run_id).lower()
today = dt.date.today()
employees, salaries = seed.generate(today)
clean_employees = [{k: v for k, v in row.items() if not k.startswith("_")} for row in employees]
atomic_json(run_dir / "employees.json", clean_employees)
atomic_json(run_dir / "salaries.json", salaries)
(run_dir / "schema.sql").write_text(POSTGRES_DDL, encoding="utf-8")
connection = psycopg2.connect(args.postgres_dsn)
version = create_schema(connection, schema, employees, salaries)
client, model, endpoint = backend(args.provider, args.model)
receipts = []
records = []
for question in QUESTIONS:
sql_attempts = []
feedback = None
prior_sql = None
actual = []
error = None
latency = 0.0
sql = ""
for attempt in range(1, 4):
sql, receipt = generate_sql(
client,
model,
question,
execution_feedback=feedback,
prior_sql=prior_sql,
attempt=attempt,
)
receipts.append(receipt)
query_started = time.monotonic()
error = None
if not re.match(r"^(SELECT|WITH)\b", sql, re.I):
error = "ReadOnlyGate: SQL must begin with SELECT or WITH"
actual = []
else:
try:
with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO "{schema}"')
cursor.execute(sql)
actual = cursor.fetchall()
except Exception as exc:
connection.rollback()
actual = []
error = f"{type(exc).__name__}: {exc}"
attempt_latency = round(time.monotonic() - query_started, 4)
latency += attempt_latency
sql_attempts.append(
{
"attempt": attempt,
"sql": sql,
"query_latency_s": attempt_latency,
"execution_error": error,
}
)
if not error:
break
prior_sql, feedback = sql, error
expected = reference.REFERENCE[question["id"]](employees, salaries, today)
passed, comparison = equal_rows(expected, actual) if not error else (False, error)
records.append({
"id": question["id"], "question": question["nl"], "sql": sql,
"sql_attempts": sql_attempts,
"rows": jsonable(actual), "row_count": len(actual), "query_latency_s": round(latency, 4),
"expected": jsonable(expected), "passed": passed, "comparison": comparison,
})
print(f"Q{question['id']}: {'PASS' if passed else 'FAIL'} {comparison}", flush=True)
connection.close()
atomic_json(run_dir / "receipts.json", receipts)
atomic_json(run_dir / "queries_and_results.json", records)
browser = render_results(run_dir, records)
prompts = json.dumps([r["request"] for r in receipts], ensure_ascii=False)
gates = {
"real_postgresql_server": "PostgreSQL" in version,
"exact_two_table_schema_created": True,
"all_10_natural_language_questions_attempted": len(records) == 10,
"all_10_sql_artifacts_are_read_only": all(re.match(r"^(SELECT|WITH)\b", r["sql"], re.I) for r in records),
"database_not_llm_received_rows": all(row["name"] not in prompts for row in clean_employees),
"database_executed_every_artifact": all(r["query_latency_s"] >= 0 and r["comparison"] for r in records),
"all_10_answers_match_independent_reference": all(r["passed"] for r in records),
"result_tables_rendered_directly_in_real_browser": bool(browser["version"] and (run_dir / "results.png").is_file()),
"raw_model_receipts_complete": len(receipts) >= 10 and all(r["response"]["id"] and r["usage"]["total_tokens"] for r in receipts),
"repairs_use_execution_errors_only": all(
r["purpose"] != "postgres_execution_error_repair"
or "PostgreSQL error" in r["request"]["messages"][-1]["content"]
for r in receipts
),
"raw_database_rows_and_hashes_retained": (run_dir / "employees.json").is_file() and (run_dir / "salaries.json").is_file(),
}
manifest = {
"schema_version": "1.0", "experiment": "5-10", "run_id": run_id,
"started_at_utc": started.isoformat(), "completed_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
"provider": args.provider, "endpoint": endpoint, "model": model,
"postgresql": {"version": version, "database": "postgres", "schema": schema, "employees": len(employees), "salary_rows": len(salaries)},
"source": {"manuscript": "book/chapter5.md#实验-5-10", "campaign_sha256": sha256(Path(__file__)), "seed_sha256": sha256(HERE / "seed.py")},
"records": records,
"browser": browser,
"usage": {
"calls": len(receipts),
"prompt_tokens": sum(r["usage"]["prompt_tokens"] or 0 for r in receipts),
"completion_tokens": sum(r["usage"]["completion_tokens"] or 0 for r in receipts),
"total_tokens": sum(r["usage"]["total_tokens"] or 0 for r in receipts),
"model_latency_s": round(sum(r["latency_s"] for r in receipts), 3),
"db_latency_s": round(sum(r["query_latency_s"] for r in records), 4),
},
"artifacts": {
name: {"path": name, "sha256": sha256(run_dir / name)}
for name in ("employees.json", "salaries.json", "schema.sql", "receipts.json", "queries_and_results.json", "results.html", "results.png")
},
"acceptance_gates": gates,
"official_complete": all(gates.values()),
}
atomic_json(run_dir / "manifest.json", manifest)
(HERE / "validation").mkdir(exist_ok=True)
if manifest["official_complete"]:
shutil.copyfile(run_dir / "manifest.json", HERE / "validation" / "latest.json")
print(json.dumps({"run_id": run_id, "official_complete": manifest["official_complete"], "passed": sum(r["passed"] for r in records)}, ensure_ascii=False, indent=2))
if not manifest["official_complete"]:
raise SystemExit(2)
if __name__ == "__main__":
main()
+375
View File
@@ -0,0 +1,375 @@
"""
实验 5-10:自然语言交互的 ERP AgentNL -> SQLartifact 模式)命令行入口。
核心思想(artifact 模式):Agent 只负责「生成 SQL 制品」,不亲自搬运数据;
真正的查询由系统用生成的 SQL 在数据库上执行,结果表直达用户界面。
子命令:
run 在线:Agent 生成 SQL -> 执行 -> 与参考实现比对(需 OPENAI_API_KEY,默认子命令)
gold 离线:执行内置「标准 SQL」跑 10 题 -> 与参考实现比对(无需 API,用于自检/演示)
ask 在线:单条自然语言查询 -> 生成 SQL -> 执行并打印结果表(需 OPENAI_API_KEY
initdb 建表并把可复现的种子数据灌入一个 SQLite 文件(离线,便于用 sqlite3 手工查看)
不带子命令时等价于 `run`,保持与旧版 `python demo.py` 相同的默认行为。
完整用法见 `python demo.py --help`,或某个子命令的 `python demo.py <子命令> --help`。
"""
import argparse
import json
import os
import sqlite3
import sys
from datetime import date
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
import seed
import reference
import gold
from questions import QUESTIONS
from agent import SQLAgent, MODEL
# ---------------- 结果比对 ----------------
def _norm(v):
"""把单个值归一化为 ('n', 数值) 或 ('s', 字符串),便于容差比对。"""
if isinstance(v, bool):
return ("n", float(v))
if isinstance(v, (int, float)):
return ("n", round(float(v), 2))
return ("s", str(v).strip())
def _row_match(a, b, tol):
if len(a) != len(b):
return False
for x, y in zip(a, b):
if x[0] != y[0]:
return False
if x[0] == "n":
if abs(x[1] - y[1]) > tol:
return False
else:
if x[1] != y[1]:
return False
return True
def compare(expected, actual, tol=0.1):
"""按多重集合(忽略行顺序)比对期望与实际结果,数值带容差。"""
exp = [tuple(_norm(v) for v in r) for r in expected]
act = [tuple(_norm(v) for v in r) for r in actual]
if len(exp) != len(act):
return False, f"行数不一致:期望 {len(exp)} 行,实际 {len(act)}"
remaining = list(act)
for er in exp:
for i, ar in enumerate(remaining):
if _row_match(er, ar, tol):
remaining.pop(i)
break
else:
return False, f"缺少匹配行:{_readable(er)}"
return True, "结果一致"
def _readable(norm_row):
return tuple(v[1] for v in norm_row)
# ---------------- 结果表打印 ----------------
def print_table(rows, max_rows=12):
if not rows:
print(" (空结果)")
return
for r in rows[:max_rows]:
cells = []
for v in r:
if isinstance(v, float):
cells.append(f"{v:.2f}")
else:
cells.append(str(v))
print(" | " + " | ".join(cells) + " |")
if len(rows) > max_rows:
print(f" ... 共 {len(rows)}")
# ---------------- 逐题执行主循环(在线/离线共用) ----------------
def run_questions(conn, employees, salaries, today, sql_provider,
qids=None, print_sql=True, max_rows=12):
"""对每个问题:取 SQL -> 执行 -> 与 Python 参考实现比对,逐题打印。
sql_provider(q) -> str:给出该题的 SQL;可能抛异常(如在线调用 LLM 失败)。
在线模式传入 `lambda q: agent.generate_sql(q["nl"], q["hint"])`
离线模式传入 `lambda q: gold.GOLD[q["id"]]`。
qids:只跑这些题号(None 表示全部)。
返回 (passed, total, results)results 为逐题明细 dict,便于 --output 导出。
"""
results = []
passed = 0
total = 0
for q in QUESTIONS:
if qids and q["id"] not in qids:
continue
total += 1
qid, nl, hint = q["id"], q["nl"], q["hint"]
print(f"\n【问题 {qid}{nl}")
rec = {"id": qid, "nl": nl, "sql": None, "rows": None,
"passed": False, "error": None}
# 1) 取 SQL 制品(在线由 Agent 生成,离线取内置 gold SQL)
try:
sql = sql_provider(q)
except Exception as e:
print(f" [生成 SQL 失败] {e}")
rec["error"] = f"生成 SQL 失败:{e}"
results.append(rec)
continue
rec["sql"] = sql
if print_sql:
print(" 生成的 SQL")
for line in sql.splitlines():
print(" " + line)
# 2) 系统执行 SQL
try:
cur = conn.cursor()
cur.execute(sql)
actual = cur.fetchall()
except Exception as e:
print(f" [SQL 执行出错] {e}")
print(" 结果:不通过 ✗")
rec["error"] = f"SQL 执行出错:{e}"
results.append(rec)
continue
rec["rows"] = [list(r) for r in actual]
print(" 查询结果:")
print_table(actual, max_rows=max_rows)
# 3) 与参考实现比对
expected = reference.REFERENCE[qid](employees, salaries, today)
ok, msg = compare(expected, actual)
rec["passed"] = ok
if ok:
passed += 1
print(f" 校验:通过 ✓({msg}")
else:
print(f" 校验:不通过 ✗({msg}")
print(f" 参考期望:{[tuple(r) for r in expected][:12]}")
results.append(rec)
return passed, total, results
# ---------------- 公用:建库、题号过滤、导出、页眉页脚 ----------------
def _build_db(db_path, today):
"""按固定种子生成数据并灌入指定的 SQLite 库(':memory:' 或文件路径)。
每次都重新灌入,保证与 reference.py 的期望答案严格对齐、结果可复现。
"""
employees, salaries = seed.generate(today)
conn = sqlite3.connect(db_path)
seed.create_db(conn, employees, salaries)
return conn, employees, salaries
def _parse_only(only):
"""'1,5,10' 解析成 {1,5,10};空/None 表示全部题目。"""
if not only:
return None
ids = set()
for part in only.split(","):
part = part.strip()
if part:
try:
ids.add(int(part))
except ValueError:
raise SystemExit(f"题号必须是整数:{part!r}--only 形如 1,5,10")
unknown = ids - {q["id"] for q in QUESTIONS}
if unknown:
raise SystemExit(f"未知题号:{sorted(unknown)}(有效题号 1~{len(QUESTIONS)}")
return ids
def _header(mode, today, employees, salaries, model=None):
print("=" * 70)
tail = f" | 模型:{model}" if model else " | 离线(不调用 API"
print(f"ERP Agent 实验 5-10 | {mode}{tail}")
print(f"今天:{today.isoformat()} | 员工 {len(employees)} 人,"
f"工资记录 {len(salaries)}")
print("=" * 70)
def _footer(passed, total):
print("\n" + "=" * 70)
rate = (passed / total * 100) if total else 0
print(f"总通过率:{passed}/{total} ({rate:.0f}%)")
print("=" * 70)
def _write_output(path, mode, today, passed, total, results):
payload = {
"experiment": "5-10 ERP Agent NL->SQL",
"mode": mode,
"date": today.isoformat(),
"passed": passed,
"total": total,
"results": results,
}
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f"\n已写出结果 JSON{path}")
def _require_api():
if not (os.environ.get("OPENAI_API_KEY") or os.environ.get("OPENROUTER_API_KEY")):
print("请先设置 OPENAI_API_KEY(或 OPENROUTER_API_KEY 兜底)环境变量(可复制 env.example 为 .env)。")
print("若只想离线跑通、不调用 API,请改用:python demo.py gold")
sys.exit(1)
# ---------------- 子命令 ----------------
def cmd_run(args):
"""在线:Agent 生成 SQL -> 执行 -> 比对。"""
_require_api()
today = date.today()
conn, emps, sals = _build_db(args.db, today)
model = args.model or os.environ.get("OPENAI_MODEL", MODEL)
_header("在线(Agent 生成 SQL", today, emps, sals, model=model)
agent = SQLAgent(model=model)
qids = _parse_only(args.only)
passed, total, results = run_questions(
conn, emps, sals, today,
sql_provider=lambda q: agent.generate_sql(q["nl"], q["hint"]),
qids=qids, max_rows=args.max_rows,
)
_footer(passed, total)
if args.output:
_write_output(args.output, "run", today, passed, total, results)
def cmd_gold(args):
"""离线:执行内置标准 SQL -> 比对(无需 API)。"""
today = date.today()
conn, emps, sals = _build_db(args.db, today)
_header("离线自检(内置 gold SQL", today, emps, sals, model=None)
qids = _parse_only(args.only)
passed, total, results = run_questions(
conn, emps, sals, today,
sql_provider=lambda q: gold.GOLD[q["id"]],
qids=qids, max_rows=args.max_rows,
)
_footer(passed, total)
if args.output:
_write_output(args.output, "gold", today, passed, total, results)
def cmd_ask(args):
"""在线:单条自然语言查询 -> 生成 SQL -> 执行并打印结果表。"""
_require_api()
today = date.today()
conn, emps, sals = _build_db(args.db, today)
model = args.model or os.environ.get("OPENAI_MODEL", MODEL)
agent = SQLAgent(model=model)
hint = args.hint or "自行判断需要返回的列;只输出一条 SELECT。"
print(f"【问题】{args.query}")
try:
sql = agent.generate_sql(args.query, hint)
except Exception as e:
print(f"[Agent 生成 SQL 失败] {e}")
sys.exit(1)
print("生成的 SQL")
for line in sql.splitlines():
print(" " + line)
try:
cur = conn.cursor()
cur.execute(sql)
rows = cur.fetchall()
except Exception as e:
print(f"[SQL 执行出错] {e}")
sys.exit(1)
print("查询结果:")
print_table(rows, max_rows=args.max_rows)
def cmd_initdb(args):
"""建表并把种子数据灌入一个 SQLite 文件,便于手工用 sqlite3 查看。"""
today = date.today()
if args.db == ":memory:":
raise SystemExit("initdb 需要一个文件路径,例如:python demo.py initdb --db erp.db")
if os.path.exists(args.db):
os.remove(args.db)
conn, emps, sals = _build_db(args.db, today)
conn.close()
print(f"已写入 SQLite 库:{args.db}")
print(f" 员工 {len(emps)} 人,工资记录 {len(sals)} 条,基准日期 {today.isoformat()}")
print(f" 手工查看: sqlite3 {args.db} \"SELECT * FROM employees LIMIT 5;\"")
print(f" 离线复跑: python demo.py gold --db {args.db}")
# ---------------- argparse CLI ----------------
def build_parser():
p = argparse.ArgumentParser(
prog="demo.py",
description="实验 5-10:自然语言交互的 ERP AgentNL -> SQLartifact 模式)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="不带子命令时等价于 run(保持旧版默认行为)。"
"离线自检不需要 APIpython demo.py gold",
)
sub = p.add_subparsers(dest="cmd", metavar="子命令")
def add_common(sp, with_model=False):
sp.add_argument("--db", default=":memory:",
help="SQLite 库:':memory:'(默认,内存库)或文件路径")
sp.add_argument("--only", default=None, metavar="题号列表",
help="只跑指定题号,逗号分隔,如 1,5,10(默认全部)")
sp.add_argument("--max-rows", type=int, default=12, dest="max_rows",
help="每题结果表最多打印多少行(默认 12")
sp.add_argument("--output", default=None, metavar="路径",
help="把逐题结果写成 JSON 文件")
if with_model:
sp.add_argument("--model", default=None,
help=f"覆盖模型(默认读 OPENAI_MODEL,否则 {MODEL}")
sp_run = sub.add_parser("run", help="在线:Agent 生成 SQL 跑 10 题并校验(需 API)")
add_common(sp_run, with_model=True)
sp_run.set_defaults(func=cmd_run)
sp_gold = sub.add_parser("gold", help="离线:执行内置标准 SQL 跑 10 题并校验(无需 API)")
add_common(sp_gold, with_model=False)
sp_gold.set_defaults(func=cmd_gold)
sp_ask = sub.add_parser("ask", help="在线:单条自然语言查询 -> SQL -> 结果表(需 API")
sp_ask.add_argument("query", help="要查询的自然语言问题,如“研发部现在有多少在职员工?”")
sp_ask.add_argument("--hint", default=None, help="可选:补充业务口径/期望返回列")
sp_ask.add_argument("--db", default=":memory:",
help="SQLite 库:':memory:'(默认)或文件路径")
sp_ask.add_argument("--max-rows", type=int, default=20, dest="max_rows",
help="结果表最多打印多少行(默认 20")
sp_ask.add_argument("--model", default=None,
help=f"覆盖模型(默认读 OPENAI_MODEL,否则 {MODEL}")
sp_ask.set_defaults(func=cmd_ask)
sp_init = sub.add_parser("initdb", help="建表并把种子数据灌入 SQLite 文件(离线)")
sp_init.add_argument("--db", default="erp.db",
help="目标 SQLite 文件路径(默认 erp.db")
sp_init.set_defaults(func=cmd_initdb)
return p
def main(argv=None):
parser = build_parser()
args = parser.parse_args(argv)
if args.cmd is None:
# 不带子命令 -> 沿用旧版默认行为:在线跑全部 10 题
args = parser.parse_args((argv or []) + ["run"])
args.func(args)
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
# 复制为 .env 后填入你的 OpenAI Key(直连,必填其一)
OPENAI_API_KEY=your-openai-api-key
# 通用兜底:未配置 OPENAI_API_KEY 时自动改走 OpenRouter
# 默认模型 gpt-5.6-lunagpt-5.x)直连 OpenAI 需组织实名认证,
# 故设置了本 key 时会优先走 OpenRouterroute openai/gpt-5.6-luna)。
# OPENROUTER_API_KEY=your-openrouter-api-key
# 可选:切换到兼容 OpenAI 协议的服务端点
# OPENAI_BASE_URL=https://api.openai.com/v1
# 可选:更换模型(默认 gpt-5.6-luna
# OPENAI_MODEL=gpt-5.6-luna
+143
View File
@@ -0,0 +1,143 @@
"""
10 道题的「标准 SQL」(gold SQL),SQLite 方言,人工编写并逐题核对过。
用途:
- 离线演示(`python demo.py gold`):不调用任何 API,直接执行这些 SQL,
证明 schema + 种子数据这套数据模型本身是自洽、可查询的;
- 作为 Agent 生成 SQL 的「参考写法」:与 reference.py(纯 Python 参考实现)
语义一致,`demo.py` 会把执行结果与 reference.py 比对,逐题打印 通过/不通过。
约定:
- 日期一律用 date('now','localtime') / strftime(...,'now','localtime') 取「今天」,
与 seed.py 里以本地 date.today() 生成的数据对齐(避免 UTC 与本地相差一天);
- **不硬编码年份**,一律从数据库当前日期用修饰符推导('-1 year' / 'start of year' 等);
- 「A部门」= 研发部,「B部门」= 销售部;「在职」= leave_date IS NULL。
"""
GOLD = {
# 1. 平均每个员工在职多久(天)。离职用 leave_date,在职用今天。
1: """
SELECT ROUND(AVG(
julianday(COALESCE(leave_date, date('now','localtime')))
- julianday(hire_date)
), 2) AS avg_tenure_days
FROM employees;
""".strip(),
# 2. 每个部门有多少在职员工。
2: """
SELECT department, COUNT(*) AS active_count
FROM employees
WHERE leave_date IS NULL
GROUP BY department;
""".strip(),
# 3. 哪个部门(含离职)平均级别最高,只返回部门名。
3: """
SELECT department
FROM employees
GROUP BY department
ORDER BY AVG(level) DESC
LIMIT 1;
""".strip(),
# 4. 每个部门今年 / 去年各新入职多少人(按 hire_date 年份)。
4: """
SELECT department,
SUM(CASE WHEN strftime('%Y', hire_date)
= strftime('%Y','now','localtime') THEN 1 ELSE 0 END) AS this_year,
SUM(CASE WHEN strftime('%Y', hire_date)
= strftime('%Y','now','localtime','-1 year') THEN 1 ELSE 0 END) AS last_year
FROM employees
GROUP BY department
HAVING this_year > 0 OR last_year > 0;
""".strip(),
# 5. 前年3月 ~ 去年5月(含两端),研发部(A部门)平均工资。
5: """
SELECT ROUND(AVG(s.salary), 2) AS avg_salary
FROM salaries s
JOIN employees e ON e.emp_id = s.emp_id
WHERE e.department = '研发部'
AND strftime('%Y-%m', s.pay_date) BETWEEN
strftime('%Y-%m','now','localtime','start of year','-2 years','+2 months')
AND strftime('%Y-%m','now','localtime','start of year','-1 year','+4 months');
""".strip(),
# 6. 去年研发部(A)与销售部(B)平均工资,两行(含已离职员工)。
6: """
SELECT e.department, ROUND(AVG(s.salary), 2) AS avg_salary
FROM salaries s
JOIN employees e ON e.emp_id = s.emp_id
WHERE e.department IN ('研发部','销售部')
AND strftime('%Y', s.pay_date) = strftime('%Y','now','localtime','-1 year')
GROUP BY e.department;
""".strip(),
# 7. 今年每个级别的员工平均工资。
7: """
SELECT e.level, ROUND(AVG(s.salary), 2) AS avg_salary
FROM salaries s
JOIN employees e ON e.emp_id = s.emp_id
WHERE strftime('%Y', s.pay_date) = strftime('%Y','now','localtime')
GROUP BY e.level;
""".strip(),
# 8. 工龄分档(入职一年内 / 一到两年 / 两到三年,三年以上不计),各档最近一月工资的平均。
8: """
WITH latest AS ( -- 每位员工「最近一个月」的工资
SELECT s.emp_id, s.salary
FROM salaries s
JOIN (SELECT emp_id, MAX(pay_date) AS mp FROM salaries GROUP BY emp_id) m
ON m.emp_id = s.emp_id AND m.mp = s.pay_date
),
bucketed AS ( -- 给每位员工打上工龄档位
SELECT e.emp_id,
CASE
WHEN julianday(date('now','localtime')) - julianday(e.hire_date) < 365 THEN '入职一年内'
WHEN julianday(date('now','localtime')) - julianday(e.hire_date) < 730 THEN '一到两年'
WHEN julianday(date('now','localtime')) - julianday(e.hire_date) < 1095 THEN '两到三年'
ELSE NULL
END AS bucket
FROM employees e
)
SELECT b.bucket, ROUND(AVG(l.salary), 2) AS avg_salary
FROM bucketed b
JOIN latest l ON l.emp_id = b.emp_id
WHERE b.bucket IS NOT NULL
GROUP BY b.bucket;
""".strip(),
# 9. 去年到今年涨薪额(今年均薪 - 去年均薪)最大的 10 人,只算两年都有工资的。
9: """
WITH ty AS (
SELECT emp_id, AVG(salary) AS a FROM salaries
WHERE strftime('%Y', pay_date) = strftime('%Y','now','localtime') GROUP BY emp_id),
ly AS (
SELECT emp_id, AVG(salary) AS a FROM salaries
WHERE strftime('%Y', pay_date) = strftime('%Y','now','localtime','-1 year') GROUP BY emp_id)
SELECT e.name, ROUND(ty.a - ly.a, 2) AS raise_amt
FROM ty
JOIN ly ON ty.emp_id = ly.emp_id
JOIN employees e ON e.emp_id = ty.emp_id
ORDER BY raise_amt DESC
LIMIT 10;
""".strip(),
# 10. 拖欠工资:某月在职却没有发薪记录。递归展开每人的在职月份再左连接工资表。
10: """
WITH RECURSIVE em(emp_id, m, end_m) AS (
SELECT emp_id,
strftime('%Y-%m', hire_date),
COALESCE(strftime('%Y-%m', leave_date), strftime('%Y-%m','now','localtime'))
FROM employees
UNION ALL
SELECT emp_id, strftime('%Y-%m', date(m || '-01', '+1 month')), end_m
FROM em WHERE m < end_m)
SELECT em.emp_id, em.m
FROM em
LEFT JOIN salaries s
ON s.emp_id = em.emp_id AND strftime('%Y-%m', s.pay_date) = em.m
WHERE s.emp_id IS NULL;
""".strip(),
}
+91
View File
@@ -0,0 +1,91 @@
"""
10 个自然语言问题,以及给 Agent 的「输出列」提示。
hint 里只补充「业务口径 + 期望返回哪些列、什么顺序」这类 schema 级提示,
不泄露具体数值答案。列顺序与 reference.py 的返回一致,便于逐行比对。
"""
QUESTIONS = [
{
"id": 1,
"nl": "平均每个员工在职多久?",
"hint": "在职时长按天计:离职员工用 leave_date,在职员工用今天 date('now')"
"对全部员工求平均。只返回一列:平均在职天数。",
},
{
"id": 2,
"nl": "每个部门有多少在职员工?",
"hint": "在职指 leave_date 为空。返回两列:部门, 在职人数。",
},
{
"id": 3,
"nl": "哪个部门员工平均级别最高?",
"hint": "按所有员工(含离职)的 level 求各部门平均,取最高的那个部门。"
"只返回一列:部门名称。",
},
{
"id": 4,
"nl": "每个部门今年和去年各新入职多少人?",
"hint": "按 hire_date 的年份统计。返回三列:部门, 今年入职人数, 去年入职人数;"
"只保留今年或去年至少有一人入职的部门。",
},
{
"id": 5,
"nl": "前年3月到去年5月,A部门平均工资是多少?",
"hint": "A部门=研发部;时间范围指发薪月份从『前年3月』到『去年5月』(含两端)。"
"禁止硬编码年份,时间范围可写成:"
"strftime('%Y-%m',pay_date) BETWEEN "
"strftime('%Y-%m','now','-2 years','start of year','+2 months') AND "
"strftime('%Y-%m','now','-1 year','start of year','+4 months')。"
"只返回一列:平均工资。",
},
{
"id": 6,
"nl": "去年A部门和B部门平均工资哪个高?",
"hint": "A部门=研发部,B部门=销售部;只统计去年发薪记录"
"strftime('%Y',pay_date)=strftime('%Y','now','-1 year'))。"
"统计部门内所有员工(含已离职),不要按 leave_date 过滤。"
"返回两列:部门, 平均工资(两行,分别对应研发部和销售部)。",
},
{
"id": 7,
"nl": "今年每个级别的员工平均工资是多少?",
"hint": "只统计今年发薪记录,按 level 分组。返回两列:级别, 平均工资。",
},
{
"id": 8,
"nl": "入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?",
"hint": "工龄按 date('now')-hire_date 的天数分档:<365 天为『入职一年内』,"
"365~730 天为『一到两年』,730~1095 天为『两到三年』,三年以上不统计。"
"『最近一个月工资』指该员工发薪日期最大的那条工资。按档位求平均。"
"返回两列:档位(值必须正好是『入职一年内』/『一到两年』/『两到三年』), 平均工资。",
},
{
"id": 9,
"nl": "去年到今年涨薪幅度最大的10位员工是谁?",
"hint": "对每位员工,涨薪额 = 今年平均工资 - 去年平均工资,只统计去年和今年都有工资的员工,"
"按涨薪额从高到低取前 10。返回两列:姓名, 涨薪额。",
},
{
"id": 10,
"nl": "有没有拖欠工资的情况(某个月还在职却没有发薪)?",
"hint": "对每位员工,其在职月份从入职月份到(离职员工用离职月份、在职员工用当前月份),"
"逐月检查是否有对应的发薪记录,找出缺失的(员工, 月份)。"
"返回两列:emp_id, 月份(格式 YYYY-MM)。"
"推荐写法(递归 CTE 里把『结束月份』一并带进去,避免相关子查询):\n"
"WITH RECURSIVE em(emp_id, m, end_m) AS (\n"
" SELECT emp_id, strftime('%Y-%m', hire_date),\n"
" COALESCE(strftime('%Y-%m', leave_date), strftime('%Y-%m','now'))\n"
" FROM employees\n"
" UNION ALL\n"
" SELECT emp_id, strftime('%Y-%m', date(m || '-01', '+1 month')), end_m\n"
" FROM em WHERE m < end_m)\n"
"SELECT em.emp_id, em.m FROM em\n"
"LEFT JOIN salaries s ON s.emp_id = em.emp_id "
"AND strftime('%Y-%m', s.pay_date) = em.m\n"
"WHERE s.emp_id IS NULL;",
},
]
# 需要「按顺序」呈现的题目(校验时其实按集合比对内容即可,这里仅用于展示)
ORDERED = {9}
+165
View File
@@ -0,0 +1,165 @@
"""
独立的 Python 参考实现:直接在种子数据(内存 list)上计算 10 个问题的期望答案。
这些函数刻意「不走 SQL」,用来校验 Agent 生成 SQL 的执行结果是否正确。
每个函数返回 list[tuple],元组内的列顺序与 questions.py 里给 Agent 的
「列顺序提示」保持一致,便于逐行比对。
"""
from datetime import date
from statistics import mean
DEPT_A = "研发部" # 题目里的「A 部门」
DEPT_B = "销售部" # 题目里的「B 部门」
def _end_date(e, today):
return e["leave_date"] if e["leave_date"] else today
def _ym(d: date):
return (d.year, d.month)
def _latest_salary(emp_id, salaries):
recs = [s for s in salaries if s["emp_id"] == emp_id]
if not recs:
return None
return max(recs, key=lambda s: s["pay_date"])["salary"]
def q1_avg_tenure_days(emps, sals, today):
days = [(_end_date(e, today) - e["hire_date"]).days for e in emps]
return [(round(mean(days), 2),)]
def q2_active_by_dept(emps, sals, today):
counts = {}
for e in emps:
if e["leave_date"] is None:
counts[e["department"]] = counts.get(e["department"], 0) + 1
return [(d, c) for d, c in counts.items()]
def q3_dept_highest_avg_level(emps, sals, today):
by_dept = {}
for e in emps:
by_dept.setdefault(e["department"], []).append(e["level"])
top = max(by_dept.items(), key=lambda kv: mean(kv[1]))
return [(top[0],)] # 只返回部门名称
def q4_hires_this_and_last_year(emps, sals, today):
y = today.year
agg = {}
for e in emps:
hy = e["hire_date"].year
if hy not in (y, y - 1):
continue
ty, ly = agg.get(e["department"], (0, 0))
if hy == y:
ty += 1
else:
ly += 1
agg[e["department"]] = (ty, ly)
return [(d, ty, ly) for d, (ty, ly) in agg.items()]
def q5_deptA_avg_salary_range(emps, sals, today):
y = today.year
lo, hi = (y - 2, 3), (y - 1, 5) # 前年3月 ~ 去年5月(含端点)
dept = {e["emp_id"] for e in emps if e["department"] == DEPT_A}
vals = [s["salary"] for s in sals
if s["emp_id"] in dept and lo <= _ym(s["pay_date"]) <= hi]
return [(round(mean(vals), 2),)]
def q6_deptAB_avg_salary_last_year(emps, sals, today):
y = today.year - 1
out = []
for dept in (DEPT_A, DEPT_B):
ids = {e["emp_id"] for e in emps if e["department"] == dept}
vals = [s["salary"] for s in sals
if s["emp_id"] in ids and s["pay_date"].year == y]
out.append((dept, round(mean(vals), 2)))
return out
def q7_avg_salary_by_level_this_year(emps, sals, today):
y = today.year
lvl = {e["emp_id"]: e["level"] for e in emps}
by_level = {}
for s in sals:
if s["pay_date"].year == y:
by_level.setdefault(lvl[s["emp_id"]], []).append(s["salary"])
return [(l, round(mean(v), 2)) for l, v in by_level.items()]
def q8_avg_latest_salary_by_tenure(emps, sals, today):
buckets = {"入职一年内": [], "一到两年": [], "两到三年": []}
for e in emps:
days = (today - e["hire_date"]).days
if days < 365:
b = "入职一年内"
elif days < 730:
b = "一到两年"
elif days < 1095:
b = "两到三年"
else:
continue
last = _latest_salary(e["emp_id"], sals)
if last is not None:
buckets[b].append(last)
return [(b, round(mean(v), 2)) for b, v in buckets.items() if v]
def q9_top10_raise(emps, sals, today):
y = today.year
name = {e["emp_id"]: e["name"] for e in emps}
this_year, last_year = {}, {}
for s in sals:
if s["pay_date"].year == y:
this_year.setdefault(s["emp_id"], []).append(s["salary"])
elif s["pay_date"].year == y - 1:
last_year.setdefault(s["emp_id"], []).append(s["salary"])
rows = []
for eid in set(this_year) & set(last_year):
raise_amt = mean(this_year[eid]) - mean(last_year[eid])
rows.append((name[eid], round(raise_amt, 2)))
rows.sort(key=lambda r: r[1], reverse=True)
return rows[:10]
def q10_owed_salary(emps, sals, today):
cur = (today.year, today.month)
by_emp = {}
for s in sals:
by_emp.setdefault(s["emp_id"], set()).add(_ym(s["pay_date"]))
out = []
for e in emps:
start = _ym(e["hire_date"])
end = _ym(e["leave_date"]) if e["leave_date"] else cur
paid = by_emp.get(e["emp_id"], set())
y, m = start
while (y, m) <= end:
if (y, m) not in paid:
out.append((e["emp_id"], f"{y:04d}-{m:02d}"))
m += 1
if m == 13:
y, m = y + 1, 1
return out
# 题号 -> 参考实现
REFERENCE = {
1: q1_avg_tenure_days,
2: q2_active_by_dept,
3: q3_dept_highest_avg_level,
4: q4_hires_this_and_last_year,
5: q5_deptA_avg_salary_range,
6: q6_deptAB_avg_salary_last_year,
7: q7_avg_salary_by_level_this_year,
8: q8_avg_latest_salary_by_tenure,
9: q9_top10_raise,
10: q10_owed_salary,
}
+4
View File
@@ -0,0 +1,4 @@
openai>=1.30.0
python-dotenv>=1.0.0
psycopg2-binary>=2.9.9
playwright>=1.45.0
+34
View File
@@ -0,0 +1,34 @@
-- 实验 5-10 ERP Agent —— 书中要求的 PostgreSQL schema(两张表)。
--
-- 本仓库的可运行演示用 SQLite(零依赖、可离线复现,见 seed.py / demo.py);
-- 这份 DDL 给出书中原文的 PostgreSQL 版本,方便迁移到真实 Postgres 环境。
-- 两种方言的表结构一致,差异主要在日期函数:
-- SQLite: strftime('%Y','now') julianday(a)-julianday(b) date('now','-1 year')
-- PostgreSQL: EXTRACT(YEAR FROM now()) (a::date - b::date) now() - interval '1 year'
--
-- 用法(需本机有 PostgreSQL):
-- createdb erp
-- psql erp -f schema_postgres.sql
DROP TABLE IF EXISTS salaries;
DROP TABLE IF EXISTS employees;
-- 员工表:ID、姓名、部门、级别(数字越大越高)、入职日期、离职日期(NULL = 在职)
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT NOT NULL,
level INTEGER NOT NULL,
hire_date DATE NOT NULL,
leave_date DATE -- NULL 表示在职
);
-- 工资表:员工ID、发薪日期(每月一条,取当月 1 号)、当月工资
CREATE TABLE salaries (
emp_id INTEGER NOT NULL REFERENCES employees(emp_id),
pay_date DATE NOT NULL, -- 每月一条,如 2025-03-01
salary INTEGER NOT NULL,
PRIMARY KEY (emp_id, pay_date)
);
CREATE INDEX idx_salaries_pay_date ON salaries (pay_date);
+183
View File
@@ -0,0 +1,183 @@
"""
生成可复现的 ERP 种子数据(员工表 + 工资表)。
设计要点(保证 10 个问题都有确定答案):
- 约 40 名员工,跨 5 个部门、多个级别;
- 工龄刻意覆盖「入职一年内 / 一到两年 / 两到三年 / 三年以上」四档(供问题 8);
- 若干已离职员工(leave_date 非空,供问题 2/6 等);
- 工资按「入职当年基准 + 每年固定涨薪额」逐月生成,
每位员工的年度涨薪额互不相同,从而问题 9「涨薪最大 10 人」排名唯一;
- 刻意为一名在职员工删掉某个月的工资记录(供问题 10「拖欠工资」);
- 所有日期以「今天」为基准相对生成,固定随机种子 42,可复现。
reference.py 直接在这些内存结构上计算期望答案,
与 Agent 生成 SQL 的执行结果比对,保证语义一致。
"""
import random
from datetime import date, timedelta
# ---- 业务常量 ----
DEPARTMENTS = ["研发部", "销售部", "市场部", "财务部", "人力资源部"]
# 各部门基准工资
DEPT_BASE = {"研发部": 15000, "销售部": 12000, "市场部": 11000, "财务部": 12000, "人力资源部": 10000}
_SURNAMES = list("赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜")
_GIVEN = list("伟芳娜秀英敏静丽强磊军洋勇艳杰娟涛明超霞平刚桂香建华志强晓东春梅国栋雪松")
def _first_of_month(d: date) -> date:
return date(d.year, d.month, 1)
def _add_month(d: date) -> date:
"""返回下个月的 1 号(d 需为某月 1 号)。"""
if d.month == 12:
return date(d.year + 1, 1, 1)
return date(d.year, d.month + 1, 1)
def _month_key(d: date) -> str:
return f"{d.year:04d}-{d.month:02d}"
def generate(today: date | None = None):
"""生成并返回 (employees, salaries) 两个 list[dict]。"""
if today is None:
today = date.today()
rng = random.Random(42)
cur_month = _first_of_month(today)
# 工龄分档(相对今天的天数区间),保证问题 8 各档都有人
# bucket: (人数, 最小天数, 最大天数)
tenure_plan = [
(8, 30, 360), # 入职一年内
(8, 370, 720), # 一到两年
(8, 740, 1080), # 两到三年
(16, 1100, 1900), # 三年以上
]
employees = []
emp_id = 0
for count, dmin, dmax in tenure_plan:
for _ in range(count):
emp_id += 1
days = rng.randint(dmin, dmax)
hire_date = today - timedelta(days=days)
dept = rng.choice(DEPARTMENTS)
level = rng.randint(3, 9)
name = rng.choice(_SURNAMES) + rng.choice(_GIVEN)
employees.append({
"emp_id": emp_id,
"name": name,
"department": dept,
"level": level,
"hire_date": hire_date,
"leave_date": None, # 先全部在职,稍后挑一部分离职
})
# 挑选约 6 名「三年以上」员工设为离职,离职日期落在过去 ~2 年内
senior = [e for e in employees if (today - e["hire_date"]).days > 1100]
for e in rng.sample(senior, 6):
# 离职日期 = 今天前 60~700 天,且晚于入职至少 200 天
leave = today - timedelta(days=rng.randint(60, 700))
if (leave - e["hire_date"]).days < 200:
leave = e["hire_date"] + timedelta(days=200)
# 落到月末,便于按月发薪对齐
e["leave_date"] = leave
# 为每位员工设定「入职当年基准工资」与「每年固定涨薪额」(互不相同)
for e in employees:
e["_start_base"] = (
DEPT_BASE[e["department"]] + e["level"] * 2000 + (e["emp_id"] % 7) * 300
)
# 涨薪额随 emp_id 严格递增,保证问题 9 排名唯一(无并列)
e["_annual_raise"] = 400 + e["emp_id"] * 45
# 指定一名在职、工龄较长的员工为「明显涨薪王」(问题 9 榜首)
big_raiser = next(
e for e in employees
if e["leave_date"] is None and (today - e["hire_date"]).days > 700
)
big_raiser["_annual_raise"] = 12000 # 远高于其他人
big_raiser["_is_big_raiser"] = True
# ---- 逐月生成工资 ----
salaries = []
for e in employees:
hire_m = _first_of_month(e["hire_date"])
end_m = _first_of_month(e["leave_date"]) if e["leave_date"] else cur_month
m = hire_m
while m <= end_m:
salary = e["_start_base"] + e["_annual_raise"] * (m.year - e["hire_date"].year)
salaries.append({
"emp_id": e["emp_id"],
"pay_date": m, # 每月 1 号代表当月发薪
"salary": int(salary),
})
m = _add_month(m)
# ---- 刻意制造一条「拖欠工资」:某在职员工某个过去月份缺发薪(问题 10)----
target = next(
e for e in employees
if e["leave_date"] is None and (today - e["hire_date"]).days > 800
)
# 删除「6 个月前」那条记录(确保它存在且不是当月)
missing_month = cur_month
for _ in range(6):
# 往前推 6 个月
y, mo = missing_month.year, missing_month.month - 1
if mo == 0:
y, mo = y - 1, 12
missing_month = date(y, mo, 1)
before = len(salaries)
salaries = [
s for s in salaries
if not (s["emp_id"] == target["emp_id"] and s["pay_date"] == missing_month)
]
assert len(salaries) == before - 1, "未能删除目标发薪记录,请检查种子逻辑"
target["_owed_month"] = _month_key(missing_month)
return employees, salaries
def create_db(conn, employees, salaries):
"""在给定 sqlite 连接上建表并灌入数据。"""
cur = conn.cursor()
cur.executescript(
"""
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS salaries;
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT NOT NULL,
level INTEGER NOT NULL, -- 级别,数字越大越高
hire_date TEXT NOT NULL, -- 入职日期 YYYY-MM-DD
leave_date TEXT -- 离职日期,NULL = 在职
);
CREATE TABLE salaries (
emp_id INTEGER NOT NULL, -- 关联 employees.emp_id
pay_date TEXT NOT NULL, -- 发薪日期 YYYY-MM-01(每月一条)
salary INTEGER NOT NULL, -- 当月工资
PRIMARY KEY (emp_id, pay_date)
);
"""
)
cur.executemany(
"INSERT INTO employees VALUES (?,?,?,?,?,?)",
[
(
e["emp_id"], e["name"], e["department"], e["level"],
e["hire_date"].isoformat(),
e["leave_date"].isoformat() if e["leave_date"] else None,
)
for e in employees
],
)
cur.executemany(
"INSERT INTO salaries VALUES (?,?,?)",
[(s["emp_id"], s["pay_date"].isoformat(), s["salary"]) for s in salaries],
)
conn.commit()
+23
View File
@@ -0,0 +1,23 @@
"""--only 参数解析:非法题号应干净退出(SystemExit),而非 ValueError 栈。"""
import pytest
from demo import _parse_only
def test_parse_only_valid():
assert _parse_only("1,5,10") == {1, 5, 10}
def test_parse_only_empty_means_all():
assert _parse_only("") is None
assert _parse_only(None) is None
def test_parse_only_non_integer_clean_exit():
with pytest.raises(SystemExit, match="整数"):
_parse_only("1,2x")
def test_parse_only_unknown_id_clean_exit():
with pytest.raises(SystemExit, match="未知题号"):
_parse_only("999")
+584
View File
@@ -0,0 +1,584 @@
{
"schema_version": "1.0",
"experiment": "5-10",
"run_id": "20260729T210334Z-5_10-postgresql",
"started_at_utc": "2026-07-29T21:03:34.066223+00:00",
"completed_at_utc": "2026-07-29T21:06:43.050217+00:00",
"provider": "ark",
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-250615",
"postgresql": {
"version": "PostgreSQL 14.13 (Homebrew) on aarch64-apple-darwin23.6.0, compiled by Apple clang version 16.0.0 (clang-1600.0.26.4), 64-bit",
"database": "postgres",
"schema": "exp5_10_20260729t210334z510postgresql",
"employees": 40,
"salary_rows": 1184
},
"source": {
"manuscript": "book/chapter5.md#实验-5-10",
"campaign_sha256": "d921a6d18683dcbcfa84f4ea08ad67a622315ddf54b05cf9a1ea048564eae209",
"seed_sha256": "833c1a09349802dfad70c84b7d568d1e06ec3d73bdef588c796b2d704a73730d"
},
"records": [
{
"id": 1,
"question": "平均每个员工在职多久?",
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"query_latency_s": 0.0015,
"execution_error": null
}
],
"rows": [
[
886.2
]
],
"row_count": 1,
"query_latency_s": 0.0015,
"expected": [
[
886.2
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 2,
"question": "每个部门有多少在职员工?",
"sql": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"query_latency_s": 0.0007,
"execution_error": null
}
],
"rows": [
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"研发部",
5
],
[
"财务部",
6
],
[
"销售部",
10
]
],
"row_count": 5,
"query_latency_s": 0.0007,
"expected": [
[
"研发部",
5
],
[
"销售部",
10
],
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"财务部",
6
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 3,
"question": "哪个部门员工平均级别最高?",
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"query_latency_s": 0.0008,
"execution_error": null
}
],
"rows": [
[
"销售部"
]
],
"row_count": 1,
"query_latency_s": 0.0008,
"expected": [
[
"销售部"
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 4,
"question": "每个部门今年和去年各新入职多少人?",
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0",
"query_latency_s": 0.0008,
"execution_error": null
}
],
"rows": [
[
"财务部",
0,
1
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"研发部",
1,
2
],
[
"市场部",
0,
4
]
],
"row_count": 5,
"query_latency_s": 0.0008,
"expected": [
[
"研发部",
1,
2
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"市场部",
0,
4
],
[
"财务部",
0,
1
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 5,
"question": "前年3月到去年5月,A部门平均工资是多少?",
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"query_latency_s": 0.0021,
"execution_error": null
}
],
"rows": [
[
27695.833333333332
]
],
"row_count": 1,
"query_latency_s": 0.0021,
"expected": [
[
27695.83
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 6,
"question": "去年A部门和B部门平均工资哪个高?",
"sql": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department",
"query_latency_s": 0.0011,
"execution_error": null
}
],
"rows": [
[
"研发部",
28569.444444444445
],
[
"销售部",
29481.55339805825
]
],
"row_count": 2,
"query_latency_s": 0.0011,
"expected": [
[
"研发部",
28569.44
],
[
"销售部",
29481.55
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 7,
"question": "今年每个级别的员工平均工资是多少?",
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"query_latency_s": 0.0014,
"execution_error": null
}
],
"rows": [
[
3,
23456.428571428572
],
[
4,
26248.823529411766
],
[
5,
34534.75
],
[
6,
28461.363636363636
],
[
7,
29935.0
],
[
8,
32242.69230769231
],
[
9,
34559.90566037736
]
],
"row_count": 7,
"query_latency_s": 0.0014,
"expected": [
[
3,
23456.43
],
[
8,
32242.69
],
[
6,
28461.36
],
[
4,
26248.82
],
[
9,
34559.91
],
[
5,
34534.75
],
[
7,
29935
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 8,
"question": "入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?",
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket",
"query_latency_s": 0.0015,
"execution_error": null
}
],
"rows": [
[
"一到两年",
27683.125
],
[
"两到三年",
31138.75
],
[
"入职一年内",
24053.75
]
],
"row_count": 3,
"query_latency_s": 0.0015,
"expected": [
[
"入职一年内",
24053.75
],
[
"一到两年",
27683.12
],
[
"两到三年",
31138.75
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 9,
"question": "去年到今年涨薪幅度最大的10位员工是谁?",
"sql": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"query_latency_s": 0.0018,
"execution_error": null
}
],
"rows": [
[
"韩平",
12000.0
],
[
"韩伟",
2200.0
],
[
"吕芳",
2155.0
],
[
"金丽",
2110.0
],
[
"许华",
2065.0
],
[
"陶松",
1975.0
],
[
"周雪",
1930.0
],
[
"吴敏",
1885.0
],
[
"张霞",
1840.0
],
[
"赵平",
1795.0
]
],
"row_count": 10,
"query_latency_s": 0.0018,
"expected": [
[
"韩平",
12000
],
[
"韩伟",
2200
],
[
"吕芳",
2155
],
[
"金丽",
2110
],
[
"许华",
2065
],
[
"陶松",
1975
],
[
"周雪",
1930
],
[
"吴敏",
1885
],
[
"张霞",
1840
],
[
"赵平",
1795
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 10,
"question": "有没有拖欠工资的情况(某个月还在职却没有发薪)?",
"sql": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL",
"query_latency_s": 0.0122,
"execution_error": null
}
],
"rows": [
[
17,
"2026-01"
]
],
"row_count": 1,
"query_latency_s": 0.0122,
"expected": [
[
17,
"2026-01"
]
],
"passed": true,
"comparison": "independent Python reference matched"
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"html": "results.html",
"screenshot": "results.png"
},
"usage": {
"calls": 10,
"prompt_tokens": 3154,
"completion_tokens": 8192,
"total_tokens": 11346,
"model_latency_s": 187.73,
"db_latency_s": 0.0239
},
"artifacts": {
"employees.json": {
"path": "employees.json",
"sha256": "7e5e237da09edb3db029e367a52569267241673a99e19d2ffddbb85a45cb59bd"
},
"salaries.json": {
"path": "salaries.json",
"sha256": "70a9dcd335d5c3e277eac92a4ae351f0f33c56ef9b2a526047cf60c98ed04228"
},
"schema.sql": {
"path": "schema.sql",
"sha256": "76a7d9f85d8bf3998729c40fa448e61e49ec24d47acba2f04580e5d2aeb27596"
},
"receipts.json": {
"path": "receipts.json",
"sha256": "0eb53eb1ac08d3b4091d02db1053e67078ac83e0f564ba8ae6f76fbc937e7a85"
},
"queries_and_results.json": {
"path": "queries_and_results.json",
"sha256": "a2be9fa715a15fe4290ee250f67a1c742ff8a3c3cff215f664a0d77b10ec8b40"
},
"results.html": {
"path": "results.html",
"sha256": "6099929004da8eb22e56f2557d3e167cd39ae2aee2b4a70d2fe61576d1980639"
},
"results.png": {
"path": "results.png",
"sha256": "9dccf840df361ad4031eb559672639876595f038f907e95987cc5c7ca550462c"
}
},
"acceptance_gates": {
"real_postgresql_server": true,
"exact_two_table_schema_created": true,
"all_10_natural_language_questions_attempted": true,
"all_10_sql_artifacts_are_read_only": true,
"database_not_llm_received_rows": true,
"database_executed_every_artifact": true,
"all_10_answers_match_independent_reference": true,
"result_tables_rendered_directly_in_real_browser": true,
"raw_model_receipts_complete": true,
"repairs_use_execution_errors_only": true,
"raw_database_rows_and_hashes_retained": true
},
"official_complete": true
}
@@ -0,0 +1,322 @@
[
{
"emp_id": 1,
"name": "秦娟",
"department": "研发部",
"level": 3,
"hire_date": "2025-08-07",
"leave_date": null
},
{
"emp_id": 2,
"name": "郑雪",
"department": "销售部",
"level": 8,
"hire_date": "2026-03-08",
"leave_date": null
},
{
"emp_id": 3,
"name": "孙芳",
"department": "人力资源部",
"level": 6,
"hire_date": "2026-05-17",
"leave_date": null
},
{
"emp_id": 4,
"name": "钱松",
"department": "销售部",
"level": 4,
"hire_date": "2026-05-14",
"leave_date": null
},
{
"emp_id": 5,
"name": "韩晓",
"department": "人力资源部",
"level": 6,
"hire_date": "2026-03-21",
"leave_date": null
},
{
"emp_id": 6,
"name": "赵军",
"department": "市场部",
"level": 9,
"hire_date": "2025-09-02",
"leave_date": null
},
{
"emp_id": 7,
"name": "陈艳",
"department": "市场部",
"level": 5,
"hire_date": "2025-11-26",
"leave_date": null
},
{
"emp_id": 8,
"name": "孔静",
"department": "研发部",
"level": 3,
"hire_date": "2026-01-09",
"leave_date": null
},
{
"emp_id": 9,
"name": "朱娜",
"department": "市场部",
"level": 7,
"hire_date": "2025-01-23",
"leave_date": null
},
{
"emp_id": 10,
"name": "孔敏",
"department": "人力资源部",
"level": 3,
"hire_date": "2024-12-02",
"leave_date": null
},
{
"emp_id": 11,
"name": "张勇",
"department": "市场部",
"level": 9,
"hire_date": "2024-10-16",
"leave_date": null
},
{
"emp_id": 12,
"name": "韩超",
"department": "研发部",
"level": 8,
"hire_date": "2025-06-20",
"leave_date": null
},
{
"emp_id": 13,
"name": "郑建",
"department": "销售部",
"level": 9,
"hire_date": "2025-06-15",
"leave_date": null
},
{
"emp_id": 14,
"name": "张军",
"department": "财务部",
"level": 8,
"hire_date": "2025-03-05",
"leave_date": null
},
{
"emp_id": 15,
"name": "秦英",
"department": "市场部",
"level": 4,
"hire_date": "2025-01-17",
"leave_date": null
},
{
"emp_id": 16,
"name": "杨军",
"department": "销售部",
"level": 7,
"hire_date": "2024-09-17",
"leave_date": null
},
{
"emp_id": 17,
"name": "韩平",
"department": "财务部",
"level": 5,
"hire_date": "2023-11-27",
"leave_date": null
},
{
"emp_id": 18,
"name": "孙平",
"department": "销售部",
"level": 9,
"hire_date": "2024-06-22",
"leave_date": null
},
{
"emp_id": 19,
"name": "沈平",
"department": "市场部",
"level": 3,
"hire_date": "2023-12-28",
"leave_date": null
},
{
"emp_id": 20,
"name": "魏磊",
"department": "财务部",
"level": 6,
"hire_date": "2024-04-03",
"leave_date": null
},
{
"emp_id": 21,
"name": "朱强",
"department": "销售部",
"level": 4,
"hire_date": "2024-03-07",
"leave_date": null
},
{
"emp_id": 22,
"name": "韩强",
"department": "财务部",
"level": 5,
"hire_date": "2023-09-26",
"leave_date": null
},
{
"emp_id": 23,
"name": "李丽",
"department": "财务部",
"level": 3,
"hire_date": "2023-11-03",
"leave_date": null
},
{
"emp_id": 24,
"name": "华英",
"department": "销售部",
"level": 9,
"hire_date": "2024-05-03",
"leave_date": null
},
{
"emp_id": 25,
"name": "魏栋",
"department": "财务部",
"level": 7,
"hire_date": "2022-06-27",
"leave_date": "2024-12-21"
},
{
"emp_id": 26,
"name": "赵丽",
"department": "人力资源部",
"level": 9,
"hire_date": "2022-11-11",
"leave_date": "2025-08-19"
},
{
"emp_id": 27,
"name": "秦刚",
"department": "人力资源部",
"level": 9,
"hire_date": "2021-08-27",
"leave_date": "2026-04-29"
},
{
"emp_id": 28,
"name": "褚东",
"department": "市场部",
"level": 6,
"hire_date": "2023-04-03",
"leave_date": "2025-09-30"
},
{
"emp_id": 29,
"name": "卫国",
"department": "市场部",
"level": 7,
"hire_date": "2023-07-23",
"leave_date": null
},
{
"emp_id": 30,
"name": "蒋磊",
"department": "市场部",
"level": 9,
"hire_date": "2023-04-09",
"leave_date": null
},
{
"emp_id": 31,
"name": "赵平",
"department": "销售部",
"level": 7,
"hire_date": "2022-07-09",
"leave_date": null
},
{
"emp_id": 32,
"name": "张霞",
"department": "研发部",
"level": 3,
"hire_date": "2022-03-13",
"leave_date": null
},
{
"emp_id": 33,
"name": "吴敏",
"department": "研发部",
"level": 4,
"hire_date": "2022-11-23",
"leave_date": null
},
{
"emp_id": 34,
"name": "周雪",
"department": "财务部",
"level": 9,
"hire_date": "2021-07-07",
"leave_date": null
},
{
"emp_id": 35,
"name": "陶松",
"department": "销售部",
"level": 4,
"hire_date": "2021-06-02",
"leave_date": "2026-03-20"
},
{
"emp_id": 36,
"name": "华艳",
"department": "市场部",
"level": 7,
"hire_date": "2023-02-07",
"leave_date": "2025-06-27"
},
{
"emp_id": 37,
"name": "许华",
"department": "销售部",
"level": 8,
"hire_date": "2022-01-20",
"leave_date": null
},
{
"emp_id": 38,
"name": "金丽",
"department": "市场部",
"level": 6,
"hire_date": "2021-09-07",
"leave_date": null
},
{
"emp_id": 39,
"name": "吕芳",
"department": "销售部",
"level": 3,
"hire_date": "2022-11-15",
"leave_date": null
},
{
"emp_id": 40,
"name": "韩伟",
"department": "人力资源部",
"level": 4,
"hire_date": "2021-12-01",
"leave_date": null
}
]
@@ -0,0 +1,477 @@
{
"schema_version": "1.0",
"experiment": "5-10",
"run_id": "20260729T205753Z-5_10-postgresql",
"started_at_utc": "2026-07-29T20:57:53.576348+00:00",
"completed_at_utc": "2026-07-29T21:00:59.416195+00:00",
"provider": "ark",
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-250615",
"postgresql": {
"version": "PostgreSQL 14.13 (Homebrew) on aarch64-apple-darwin23.6.0, compiled by Apple clang version 16.0.0 (clang-1600.0.26.4), 64-bit",
"database": "postgres",
"schema": "exp5_10_20260729t205753z510postgresql",
"employees": 40,
"salary_rows": 1184
},
"source": {
"manuscript": "book/chapter5.md#实验-5-10",
"campaign_sha256": "86afa359e0fca72397df53ca52149e11b2c5b0522001f583e56193e1d7eecbd2",
"seed_sha256": "833c1a09349802dfad70c84b7d568d1e06ec3d73bdef588c796b2d704a73730d"
},
"records": [
{
"id": 1,
"question": "平均每个员工在职多久?",
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"rows": [
[
886.2
]
],
"row_count": 1,
"query_latency_s": 0.0036,
"expected": [
[
886.2
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 2,
"question": "每个部门有多少在职员工?",
"sql": "SELECT department, COUNT(*) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"rows": [
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"研发部",
5
],
[
"财务部",
6
],
[
"销售部",
10
]
],
"row_count": 5,
"query_latency_s": 0.0023,
"expected": [
[
"研发部",
5
],
[
"销售部",
10
],
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"财务部",
6
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 3,
"question": "哪个部门员工平均级别最高?",
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"rows": [
[
"销售部"
]
],
"row_count": 1,
"query_latency_s": 0.0023,
"expected": [
[
"销售部"
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 4,
"question": "每个部门今年和去年各新入职多少人?",
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS this_year_count,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS last_year_count\nFROM employees\nGROUP BY department\nHAVING this_year_count > 0 OR last_year_count > 0",
"rows": [],
"row_count": 0,
"query_latency_s": 0.0047,
"expected": [
[
"研发部",
1,
2
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"市场部",
0,
4
],
[
"财务部",
0,
1
]
],
"passed": false,
"comparison": "UndefinedColumn: column \"this_year_count\" does not exist\nLINE 6: HAVING this_year_count > 0 OR last_year_count > 0\n ^\n"
},
{
"id": 5,
"question": "前年3月到去年5月,A部门平均工资是多少?",
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"rows": [
[
27695.833333333332
]
],
"row_count": 1,
"query_latency_s": 0.0038,
"expected": [
[
27695.83
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 6,
"question": "去年A部门和B部门平均工资哪个高?",
"sql": "SELECT e.department, AVG(s.salary) AS average_salary\nFROM employees e\nINNER JOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department IN ('研发部', '销售部')\n AND s.pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND s.pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY e.department",
"rows": [
[
"研发部",
28569.444444444445
],
[
"销售部",
29481.55339805825
]
],
"row_count": 2,
"query_latency_s": 0.0054,
"expected": [
[
"研发部",
28569.44
],
[
"销售部",
29481.55
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 7,
"question": "今年每个级别的员工平均工资是多少?",
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"rows": [
[
3,
23456.428571428572
],
[
4,
26248.823529411766
],
[
5,
34534.75
],
[
6,
28461.363636363636
],
[
7,
29935.0
],
[
8,
32242.69230769231
],
[
9,
34559.90566037736
]
],
"row_count": 7,
"query_latency_s": 0.0016,
"expected": [
[
3,
23456.43
],
[
8,
32242.69
],
[
6,
28461.36
],
[
4,
26248.82
],
[
9,
34559.91
],
[
5,
34534.75
],
[
7,
29935
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 8,
"question": "入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?",
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER <= 1094\nGROUP BY bucket",
"rows": [
[
"一到两年",
27683.125
],
[
"两到三年",
31138.75
],
[
"入职一年内",
24053.75
]
],
"row_count": 3,
"query_latency_s": 0.0018,
"expected": [
[
"入职一年内",
24053.75
],
[
"一到两年",
27683.12
],
[
"两到三年",
31138.75
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 9,
"question": "去年到今年涨薪幅度最大的10位员工是谁?",
"sql": "WITH employee_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS avg_prev_year,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS avg_curr_year\n FROM salaries\n GROUP BY emp_id\n HAVING AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n)\nSELECT e.name, (esa.avg_curr_year - esa.avg_prev_year) AS raise_amount\nFROM employee_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"rows": [
[
"韩平",
12000.0
],
[
"韩伟",
2200.0
],
[
"吕芳",
2155.0
],
[
"金丽",
2110.0
],
[
"许华",
2065.0
],
[
"陶松",
1975.0
],
[
"周雪",
1930.0
],
[
"吴敏",
1885.0
],
[
"张霞",
1840.0
],
[
"赵平",
1795.0
]
],
"row_count": 10,
"query_latency_s": 0.0016,
"expected": [
[
"韩平",
12000
],
[
"韩伟",
2200
],
[
"吕芳",
2155
],
[
"金丽",
2110
],
[
"许华",
2065
],
[
"陶松",
1975
],
[
"周雪",
1930
],
[
"吴敏",
1885
],
[
"张霞",
1840
],
[
"赵平",
1795
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 10,
"question": "有没有拖欠工资的情况(某个月还在职却没有发薪)?",
"sql": "WITH employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.month, 'YYYY-MM') AS month\nFROM employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND em.month = date_trunc('month', s.pay_date)\nWHERE s.emp_id IS NULL",
"rows": [
[
17,
"2026-01"
]
],
"row_count": 1,
"query_latency_s": 0.0156,
"expected": [
[
17,
"2026-01"
]
],
"passed": true,
"comparison": "independent Python reference matched"
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"html": "results.html",
"screenshot": "results.png"
},
"usage": {
"calls": 10,
"prompt_tokens": 3154,
"completion_tokens": 8642,
"total_tokens": 11796,
"model_latency_s": 184.439,
"db_latency_s": 0.0427
},
"artifacts": {
"employees.json": {
"path": "employees.json",
"sha256": "7e5e237da09edb3db029e367a52569267241673a99e19d2ffddbb85a45cb59bd"
},
"salaries.json": {
"path": "salaries.json",
"sha256": "70a9dcd335d5c3e277eac92a4ae351f0f33c56ef9b2a526047cf60c98ed04228"
},
"schema.sql": {
"path": "schema.sql",
"sha256": "76a7d9f85d8bf3998729c40fa448e61e49ec24d47acba2f04580e5d2aeb27596"
},
"receipts.json": {
"path": "receipts.json",
"sha256": "dd1009c0972f1b4a1a4509b5101d8b60f5de2058d6f6da60fdc0017e4c04e981"
},
"queries_and_results.json": {
"path": "queries_and_results.json",
"sha256": "4016b2cc9ef358262e5102604e05c2b6be187d53a74bd6b11a18dc386aea0465"
},
"results.html": {
"path": "results.html",
"sha256": "6ca358d98be1d2dd8de38d63e94c5793bb491a3a28f007f8cbff251f79d41415"
},
"results.png": {
"path": "results.png",
"sha256": "e7305970e395fb497a3d9eaf136e91cb89fe6adedbe4db65894e64234eb8d38d"
}
},
"acceptance_gates": {
"real_postgresql_server": true,
"exact_two_table_schema_created": true,
"all_10_natural_language_questions_attempted": true,
"all_10_sql_artifacts_are_read_only": true,
"database_not_llm_received_rows": true,
"database_executed_every_artifact": true,
"all_10_answers_match_independent_reference": false,
"result_tables_rendered_directly_in_real_browser": true,
"raw_model_receipts_complete": true,
"raw_database_rows_and_hashes_retained": true
},
"official_complete": false
}
@@ -0,0 +1,398 @@
[
{
"id": 1,
"question": "平均每个员工在职多久?",
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"rows": [
[
886.2
]
],
"row_count": 1,
"query_latency_s": 0.0036,
"expected": [
[
886.2
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 2,
"question": "每个部门有多少在职员工?",
"sql": "SELECT department, COUNT(*) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"rows": [
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"研发部",
5
],
[
"财务部",
6
],
[
"销售部",
10
]
],
"row_count": 5,
"query_latency_s": 0.0023,
"expected": [
[
"研发部",
5
],
[
"销售部",
10
],
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"财务部",
6
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 3,
"question": "哪个部门员工平均级别最高?",
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"rows": [
[
"销售部"
]
],
"row_count": 1,
"query_latency_s": 0.0023,
"expected": [
[
"销售部"
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 4,
"question": "每个部门今年和去年各新入职多少人?",
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS this_year_count,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS last_year_count\nFROM employees\nGROUP BY department\nHAVING this_year_count > 0 OR last_year_count > 0",
"rows": [],
"row_count": 0,
"query_latency_s": 0.0047,
"expected": [
[
"研发部",
1,
2
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"市场部",
0,
4
],
[
"财务部",
0,
1
]
],
"passed": false,
"comparison": "UndefinedColumn: column \"this_year_count\" does not exist\nLINE 6: HAVING this_year_count > 0 OR last_year_count > 0\n ^\n"
},
{
"id": 5,
"question": "前年3月到去年5月,A部门平均工资是多少?",
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"rows": [
[
27695.833333333332
]
],
"row_count": 1,
"query_latency_s": 0.0038,
"expected": [
[
27695.83
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 6,
"question": "去年A部门和B部门平均工资哪个高?",
"sql": "SELECT e.department, AVG(s.salary) AS average_salary\nFROM employees e\nINNER JOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department IN ('研发部', '销售部')\n AND s.pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND s.pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY e.department",
"rows": [
[
"研发部",
28569.444444444445
],
[
"销售部",
29481.55339805825
]
],
"row_count": 2,
"query_latency_s": 0.0054,
"expected": [
[
"研发部",
28569.44
],
[
"销售部",
29481.55
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 7,
"question": "今年每个级别的员工平均工资是多少?",
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"rows": [
[
3,
23456.428571428572
],
[
4,
26248.823529411766
],
[
5,
34534.75
],
[
6,
28461.363636363636
],
[
7,
29935.0
],
[
8,
32242.69230769231
],
[
9,
34559.90566037736
]
],
"row_count": 7,
"query_latency_s": 0.0016,
"expected": [
[
3,
23456.43
],
[
8,
32242.69
],
[
6,
28461.36
],
[
4,
26248.82
],
[
9,
34559.91
],
[
5,
34534.75
],
[
7,
29935
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 8,
"question": "入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?",
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER <= 1094\nGROUP BY bucket",
"rows": [
[
"一到两年",
27683.125
],
[
"两到三年",
31138.75
],
[
"入职一年内",
24053.75
]
],
"row_count": 3,
"query_latency_s": 0.0018,
"expected": [
[
"入职一年内",
24053.75
],
[
"一到两年",
27683.12
],
[
"两到三年",
31138.75
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 9,
"question": "去年到今年涨薪幅度最大的10位员工是谁?",
"sql": "WITH employee_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS avg_prev_year,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS avg_curr_year\n FROM salaries\n GROUP BY emp_id\n HAVING AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n)\nSELECT e.name, (esa.avg_curr_year - esa.avg_prev_year) AS raise_amount\nFROM employee_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"rows": [
[
"韩平",
12000.0
],
[
"韩伟",
2200.0
],
[
"吕芳",
2155.0
],
[
"金丽",
2110.0
],
[
"许华",
2065.0
],
[
"陶松",
1975.0
],
[
"周雪",
1930.0
],
[
"吴敏",
1885.0
],
[
"张霞",
1840.0
],
[
"赵平",
1795.0
]
],
"row_count": 10,
"query_latency_s": 0.0016,
"expected": [
[
"韩平",
12000
],
[
"韩伟",
2200
],
[
"吕芳",
2155
],
[
"金丽",
2110
],
[
"许华",
2065
],
[
"陶松",
1975
],
[
"周雪",
1930
],
[
"吴敏",
1885
],
[
"张霞",
1840
],
[
"赵平",
1795
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 10,
"question": "有没有拖欠工资的情况(某个月还在职却没有发薪)?",
"sql": "WITH employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.month, 'YYYY-MM') AS month\nFROM employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND em.month = date_trunc('month', s.pay_date)\nWHERE s.emp_id IS NULL",
"rows": [
[
17,
"2026-01"
]
],
"row_count": 1,
"query_latency_s": 0.0156,
"expected": [
[
17,
"2026-01"
]
],
"passed": true,
"comparison": "independent Python reference matched"
}
]
@@ -0,0 +1,312 @@
[
{
"question_id": 1,
"called_at_utc": "2026-07-29T20:58:02.611446+00:00",
"latency_s": 8.876,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 平均每个员工在职多久?\nPostgreSQL guidance: Use COALESCE(leave_date, CURRENT_DATE) - hire_date to obtain integer days, then AVG. Return one numeric column."
}
],
"temperature": 0
},
"response": {
"id": "021785358674075ffa263910bfaab0198f27d0234753c499bd375",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees;"
},
"usage": {
"prompt_tokens": 299,
"completion_tokens": 368,
"total_tokens": 667,
"cached_prompt_tokens": 0
}
},
{
"question_id": 2,
"called_at_utc": "2026-07-29T20:58:08.707903+00:00",
"latency_s": 6.092,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 每个部门有多少在职员工?\nPostgreSQL guidance: Active means leave_date IS NULL. GROUP BY department. Return department and active count."
}
],
"temperature": 0
},
"response": {
"id": "021785358682804ffa263910bfaab0198f27d0234753c495613be",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department, COUNT(*) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department;"
},
"usage": {
"prompt_tokens": 288,
"completion_tokens": 220,
"total_tokens": 508,
"cached_prompt_tokens": 0
}
},
{
"question_id": 3,
"called_at_utc": "2026-07-29T20:58:18.049378+00:00",
"latency_s": 9.338,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 哪个部门员工平均级别最高?\nPostgreSQL guidance: Average level across all employees by department; ORDER BY the average descending and LIMIT 1. Return department only."
}
],
"temperature": 0
},
"response": {
"id": "021785358688901ffa263910bfaab0198f27d0234753c496b22c4",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1;"
},
"usage": {
"prompt_tokens": 293,
"completion_tokens": 350,
"total_tokens": 643,
"cached_prompt_tokens": 0
}
},
{
"question_id": 4,
"called_at_utc": "2026-07-29T20:58:35.684731+00:00",
"latency_s": 17.632,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 每个部门今年和去年各新入职多少人?\nPostgreSQL guidance: Use COUNT(*) FILTER with EXTRACT(YEAR FROM hire_date) for current and previous years. Return department, this-year count, last-year count; omit departments with both zero."
}
],
"temperature": 0
},
"response": {
"id": "021785358698242ffa263910bfaab0198f27d0234753c4946649a",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS this_year_count,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS last_year_count\nFROM employees\nGROUP BY department\nHAVING this_year_count > 0 OR last_year_count > 0;"
},
"usage": {
"prompt_tokens": 312,
"completion_tokens": 959,
"total_tokens": 1271,
"cached_prompt_tokens": 0
}
},
{
"question_id": 5,
"called_at_utc": "2026-07-29T20:59:06.784757+00:00",
"latency_s": 31.095,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 前年3月到去年5月,A部门平均工资是多少?\nPostgreSQL guidance: A=研发部. Inclusive dates are March 1 two years ago through May 31 last year; derive years from CURRENT_DATE with make_date, never literals. Return AVG(salary)."
}
],
"temperature": 0
},
"response": {
"id": "021785358715879ffa263910bfaab0198f27d0234753c49eb86ce",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31);"
},
"usage": {
"prompt_tokens": 322,
"completion_tokens": 1379,
"total_tokens": 1701,
"cached_prompt_tokens": 0
}
},
{
"question_id": 6,
"called_at_utc": "2026-07-29T20:59:24.465770+00:00",
"latency_s": 17.677,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 去年A部门和B部门平均工资哪个高?\nPostgreSQL guidance: A=研发部 and B=销售部. Join employees to salaries; filter pay_date to previous calendar year, group by department, and return department plus average salary for exactly those two departments."
}
],
"temperature": 0
},
"response": {
"id": "021785358746977ffa263910bfaab0198f27d0234753c49b32464",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT e.department, AVG(s.salary) AS average_salary\nFROM employees e\nINNER JOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department IN ('研发部', '销售部')\n AND s.pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND s.pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY e.department;"
},
"usage": {
"prompt_tokens": 314,
"completion_tokens": 876,
"total_tokens": 1190,
"cached_prompt_tokens": 0
}
},
{
"question_id": 7,
"called_at_utc": "2026-07-29T20:59:35.491705+00:00",
"latency_s": 11.019,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 今年每个级别的员工平均工资是多少?\nPostgreSQL guidance: Join salary rows to employees, filter pay_date to current calendar year, group by level. Return level and average salary."
}
],
"temperature": 0
},
"response": {
"id": "021785358764663ffa263910bfaab0198f27d0234753c498b89c8",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level;"
},
"usage": {
"prompt_tokens": 298,
"completion_tokens": 448,
"total_tokens": 746,
"cached_prompt_tokens": 0
}
},
{
"question_id": 8,
"called_at_utc": "2026-07-29T21:00:05.332452+00:00",
"latency_s": 29.826,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?\nPostgreSQL guidance: First select each employee's latest salary with DISTINCT ON (emp_id) ordered by pay_date DESC. Bucket CURRENT_DATE-hire_date as <365 入职一年内, 365..729 一到两年, 730..1094 两到三年; exclude older. Return bucket and average latest salary."
}
],
"temperature": 0
},
"response": {
"id": "021785358775696ffa263910bfaab0198f27d0234753c493f16ce",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER <= 1094\nGROUP BY bucket;"
},
"usage": {
"prompt_tokens": 360,
"completion_tokens": 1449,
"total_tokens": 1809,
"cached_prompt_tokens": 0
}
},
{
"question_id": 9,
"called_at_utc": "2026-07-29T21:00:32.168574+00:00",
"latency_s": 26.832,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 去年到今年涨薪幅度最大的10位员工是谁?\nPostgreSQL guidance: Aggregate each employee's average salary separately for current and previous calendar years with FILTER, keep employees having both, compute current minus previous, order descending, LIMIT 10. Return name and raise amount."
}
],
"temperature": 0
},
"response": {
"id": "021785358805525ffa263910bfaab0198f27d0234753c494ceef9",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "WITH employee_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS avg_prev_year,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS avg_curr_year\n FROM salaries\n GROUP BY emp_id\n HAVING AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n)\nSELECT e.name, (esa.avg_curr_year - esa.avg_prev_year) AS raise_amount\nFROM employee_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10;"
},
"usage": {
"prompt_tokens": 320,
"completion_tokens": 1503,
"total_tokens": 1823,
"cached_prompt_tokens": 0
}
},
{
"question_id": 10,
"called_at_utc": "2026-07-29T21:00:58.224523+00:00",
"latency_s": 26.052,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 有没有拖欠工资的情况(某个月还在职却没有发薪)?\nPostgreSQL guidance: For every employee generate each employed month with LATERAL generate_series(date_trunc('month', hire_date), date_trunc('month', COALESCE(leave_date,CURRENT_DATE)), interval '1 month'); left join salaries by emp_id and month. Return missing emp_id and to_char(month,'YYYY-MM')."
}
],
"temperature": 0
},
"response": {
"id": "021785358832360ffa263910bfaab0198f27d0234753c49b55777",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "WITH employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.month, 'YYYY-MM') AS month\nFROM employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND em.month = date_trunc('month', s.pay_date)\nWHERE s.emp_id IS NULL;"
},
"usage": {
"prompt_tokens": 348,
"completion_tokens": 1090,
"total_tokens": 1438,
"cached_prompt_tokens": 0
}
}
]
@@ -0,0 +1,77 @@
<!doctype html><meta charset=utf-8><title>Experiment 5-10 PostgreSQL artifacts</title>
<style>body{font-family:system-ui;margin:30px;background:#f7f8fa;color:#172033}section{background:white;padding:18px;margin:16px 0;border-radius:12px}table{border-collapse:collapse}td{border:1px solid #ccd3dd;padding:5px 9px}pre{white-space:pre-wrap;background:#eef2f7;padding:12px}.ok{color:#08783e}.bad{color:#b42318}</style>
<h1>ERP Agent: SQL artifacts executed by PostgreSQL</h1><section><h2>1. 平均每个员工在职多久?</h2><pre>SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees</pre><table><tr><td>886.2</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>2. 每个部门有多少在职员工?</h2><pre>SELECT department, COUNT(*) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department</pre><table><tr><td>人力资源部</td><td>4</td></tr><tr><td>市场部</td><td>9</td></tr><tr><td>研发部</td><td>5</td></tr><tr><td>财务部</td><td>6</td></tr><tr><td>销售部</td><td>10</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>3. 哪个部门员工平均级别最高?</h2><pre>SELECT department
FROM employees
GROUP BY department
ORDER BY AVG(level) DESC
LIMIT 1</pre><table><tr><td>销售部</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>4. 每个部门今年和去年各新入职多少人?</h2><pre>SELECT department,
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS this_year_count,
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS last_year_count
FROM employees
GROUP BY department
HAVING this_year_count &gt; 0 OR last_year_count &gt; 0</pre><p>(no rows)</p><p class=bad>FAIL: UndefinedColumn: column &quot;this_year_count&quot; does not exist
LINE 6: HAVING this_year_count &gt; 0 OR last_year_count &gt; 0
^
</p></section><section><h2>5. 前年3月到去年5月,A部门平均工资是多少?</h2><pre>SELECT AVG(s.salary)
FROM employees e
JOIN salaries s ON e.emp_id = s.emp_id
WHERE e.department = &#x27;研发部&#x27;
AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)
AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)</pre><table><tr><td>27695.833333333332</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>6. 去年A部门和B部门平均工资哪个高?</h2><pre>SELECT e.department, AVG(s.salary) AS average_salary
FROM employees e
INNER JOIN salaries s ON e.emp_id = s.emp_id
WHERE e.department IN (&#x27;研发部&#x27;, &#x27;销售部&#x27;)
AND s.pay_date &gt;= date_trunc(&#x27;year&#x27;, CURRENT_DATE) - INTERVAL &#x27;1 year&#x27;
AND s.pay_date &lt; date_trunc(&#x27;year&#x27;, CURRENT_DATE)
GROUP BY e.department</pre><table><tr><td>研发部</td><td>28569.444444444445</td></tr><tr><td>销售部</td><td>29481.55339805825</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>7. 今年每个级别的员工平均工资是多少?</h2><pre>SELECT e.level, AVG(s.salary) AS average_salary
FROM employees e
JOIN salaries s ON e.emp_id = s.emp_id
WHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)
GROUP BY e.level</pre><table><tr><td>3</td><td>23456.428571428572</td></tr><tr><td>4</td><td>26248.823529411766</td></tr><tr><td>5</td><td>34534.75</td></tr><tr><td>6</td><td>28461.363636363636</td></tr><tr><td>7</td><td>29935.0</td></tr><tr><td>8</td><td>32242.69230769231</td></tr><tr><td>9</td><td>34559.90566037736</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>8. 入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?</h2><pre>WITH latest_salaries AS (
SELECT DISTINCT ON (emp_id) emp_id, salary
FROM salaries
ORDER BY emp_id, pay_date DESC
)
SELECT
CASE
WHEN (CURRENT_DATE - e.hire_date)::INTEGER &lt; 365 THEN &#x27;入职一年内&#x27;
WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN &#x27;一到两年&#x27;
WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN &#x27;两到三年&#x27;
END AS bucket,
AVG(ls.salary) AS average_latest_salary
FROM employees e
JOIN latest_salaries ls ON e.emp_id = ls.emp_id
WHERE e.leave_date IS NULL
AND (CURRENT_DATE - e.hire_date)::INTEGER &lt;= 1094
GROUP BY bucket</pre><table><tr><td>一到两年</td><td>27683.125</td></tr><tr><td>两到三年</td><td>31138.75</td></tr><tr><td>入职一年内</td><td>24053.75</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>9. 去年到今年涨薪幅度最大的10位员工是谁?</h2><pre>WITH employee_salary_avg AS (
SELECT
emp_id,
AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS avg_prev_year,
AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS avg_curr_year
FROM salaries
GROUP BY emp_id
HAVING AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL
AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL
)
SELECT e.name, (esa.avg_curr_year - esa.avg_prev_year) AS raise_amount
FROM employee_salary_avg esa
JOIN employees e ON esa.emp_id = e.emp_id
ORDER BY raise_amount DESC
LIMIT 10</pre><table><tr><td>韩平</td><td>12000.0</td></tr><tr><td>韩伟</td><td>2200.0</td></tr><tr><td>吕芳</td><td>2155.0</td></tr><tr><td>金丽</td><td>2110.0</td></tr><tr><td>许华</td><td>2065.0</td></tr><tr><td>陶松</td><td>1975.0</td></tr><tr><td>周雪</td><td>1930.0</td></tr><tr><td>吴敏</td><td>1885.0</td></tr><tr><td>张霞</td><td>1840.0</td></tr><tr><td>赵平</td><td>1795.0</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>10. 有没有拖欠工资的情况(某个月还在职却没有发薪)?</h2><pre>WITH employed_months AS (
SELECT
e.emp_id,
generate_series(
date_trunc(&#x27;month&#x27;, e.hire_date),
date_trunc(&#x27;month&#x27;, COALESCE(e.leave_date, CURRENT_DATE)),
interval &#x27;1 month&#x27;
) AS month
FROM employees e
)
SELECT
em.emp_id,
to_char(em.month, &#x27;YYYY-MM&#x27;) AS month
FROM employed_months em
LEFT JOIN salaries s
ON em.emp_id = s.emp_id
AND em.month = date_trunc(&#x27;month&#x27;, s.pay_date)
WHERE s.emp_id IS NULL</pre><table><tr><td>17</td><td>2026-01</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

@@ -0,0 +1,34 @@
-- 实验 5-10 ERP Agent —— 书中要求的 PostgreSQL schema(两张表)。
--
-- 本仓库的可运行演示用 SQLite(零依赖、可离线复现,见 seed.py / demo.py);
-- 这份 DDL 给出书中原文的 PostgreSQL 版本,方便迁移到真实 Postgres 环境。
-- 两种方言的表结构一致,差异主要在日期函数:
-- SQLite: strftime('%Y','now') julianday(a)-julianday(b) date('now','-1 year')
-- PostgreSQL: EXTRACT(YEAR FROM now()) (a::date - b::date) now() - interval '1 year'
--
-- 用法(需本机有 PostgreSQL):
-- createdb erp
-- psql erp -f schema_postgres.sql
DROP TABLE IF EXISTS salaries;
DROP TABLE IF EXISTS employees;
-- 员工表:ID、姓名、部门、级别(数字越大越高)、入职日期、离职日期(NULL = 在职)
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT NOT NULL,
level INTEGER NOT NULL,
hire_date DATE NOT NULL,
leave_date DATE -- NULL 表示在职
);
-- 工资表:员工ID、发薪日期(每月一条,取当月 1 号)、当月工资
CREATE TABLE salaries (
emp_id INTEGER NOT NULL REFERENCES employees(emp_id),
pay_date DATE NOT NULL, -- 每月一条,如 2025-03-01
salary INTEGER NOT NULL,
PRIMARY KEY (emp_id, pay_date)
);
CREATE INDEX idx_salaries_pay_date ON salaries (pay_date);
@@ -0,0 +1,322 @@
[
{
"emp_id": 1,
"name": "秦娟",
"department": "研发部",
"level": 3,
"hire_date": "2025-08-07",
"leave_date": null
},
{
"emp_id": 2,
"name": "郑雪",
"department": "销售部",
"level": 8,
"hire_date": "2026-03-08",
"leave_date": null
},
{
"emp_id": 3,
"name": "孙芳",
"department": "人力资源部",
"level": 6,
"hire_date": "2026-05-17",
"leave_date": null
},
{
"emp_id": 4,
"name": "钱松",
"department": "销售部",
"level": 4,
"hire_date": "2026-05-14",
"leave_date": null
},
{
"emp_id": 5,
"name": "韩晓",
"department": "人力资源部",
"level": 6,
"hire_date": "2026-03-21",
"leave_date": null
},
{
"emp_id": 6,
"name": "赵军",
"department": "市场部",
"level": 9,
"hire_date": "2025-09-02",
"leave_date": null
},
{
"emp_id": 7,
"name": "陈艳",
"department": "市场部",
"level": 5,
"hire_date": "2025-11-26",
"leave_date": null
},
{
"emp_id": 8,
"name": "孔静",
"department": "研发部",
"level": 3,
"hire_date": "2026-01-09",
"leave_date": null
},
{
"emp_id": 9,
"name": "朱娜",
"department": "市场部",
"level": 7,
"hire_date": "2025-01-23",
"leave_date": null
},
{
"emp_id": 10,
"name": "孔敏",
"department": "人力资源部",
"level": 3,
"hire_date": "2024-12-02",
"leave_date": null
},
{
"emp_id": 11,
"name": "张勇",
"department": "市场部",
"level": 9,
"hire_date": "2024-10-16",
"leave_date": null
},
{
"emp_id": 12,
"name": "韩超",
"department": "研发部",
"level": 8,
"hire_date": "2025-06-20",
"leave_date": null
},
{
"emp_id": 13,
"name": "郑建",
"department": "销售部",
"level": 9,
"hire_date": "2025-06-15",
"leave_date": null
},
{
"emp_id": 14,
"name": "张军",
"department": "财务部",
"level": 8,
"hire_date": "2025-03-05",
"leave_date": null
},
{
"emp_id": 15,
"name": "秦英",
"department": "市场部",
"level": 4,
"hire_date": "2025-01-17",
"leave_date": null
},
{
"emp_id": 16,
"name": "杨军",
"department": "销售部",
"level": 7,
"hire_date": "2024-09-17",
"leave_date": null
},
{
"emp_id": 17,
"name": "韩平",
"department": "财务部",
"level": 5,
"hire_date": "2023-11-27",
"leave_date": null
},
{
"emp_id": 18,
"name": "孙平",
"department": "销售部",
"level": 9,
"hire_date": "2024-06-22",
"leave_date": null
},
{
"emp_id": 19,
"name": "沈平",
"department": "市场部",
"level": 3,
"hire_date": "2023-12-28",
"leave_date": null
},
{
"emp_id": 20,
"name": "魏磊",
"department": "财务部",
"level": 6,
"hire_date": "2024-04-03",
"leave_date": null
},
{
"emp_id": 21,
"name": "朱强",
"department": "销售部",
"level": 4,
"hire_date": "2024-03-07",
"leave_date": null
},
{
"emp_id": 22,
"name": "韩强",
"department": "财务部",
"level": 5,
"hire_date": "2023-09-26",
"leave_date": null
},
{
"emp_id": 23,
"name": "李丽",
"department": "财务部",
"level": 3,
"hire_date": "2023-11-03",
"leave_date": null
},
{
"emp_id": 24,
"name": "华英",
"department": "销售部",
"level": 9,
"hire_date": "2024-05-03",
"leave_date": null
},
{
"emp_id": 25,
"name": "魏栋",
"department": "财务部",
"level": 7,
"hire_date": "2022-06-27",
"leave_date": "2024-12-21"
},
{
"emp_id": 26,
"name": "赵丽",
"department": "人力资源部",
"level": 9,
"hire_date": "2022-11-11",
"leave_date": "2025-08-19"
},
{
"emp_id": 27,
"name": "秦刚",
"department": "人力资源部",
"level": 9,
"hire_date": "2021-08-27",
"leave_date": "2026-04-29"
},
{
"emp_id": 28,
"name": "褚东",
"department": "市场部",
"level": 6,
"hire_date": "2023-04-03",
"leave_date": "2025-09-30"
},
{
"emp_id": 29,
"name": "卫国",
"department": "市场部",
"level": 7,
"hire_date": "2023-07-23",
"leave_date": null
},
{
"emp_id": 30,
"name": "蒋磊",
"department": "市场部",
"level": 9,
"hire_date": "2023-04-09",
"leave_date": null
},
{
"emp_id": 31,
"name": "赵平",
"department": "销售部",
"level": 7,
"hire_date": "2022-07-09",
"leave_date": null
},
{
"emp_id": 32,
"name": "张霞",
"department": "研发部",
"level": 3,
"hire_date": "2022-03-13",
"leave_date": null
},
{
"emp_id": 33,
"name": "吴敏",
"department": "研发部",
"level": 4,
"hire_date": "2022-11-23",
"leave_date": null
},
{
"emp_id": 34,
"name": "周雪",
"department": "财务部",
"level": 9,
"hire_date": "2021-07-07",
"leave_date": null
},
{
"emp_id": 35,
"name": "陶松",
"department": "销售部",
"level": 4,
"hire_date": "2021-06-02",
"leave_date": "2026-03-20"
},
{
"emp_id": 36,
"name": "华艳",
"department": "市场部",
"level": 7,
"hire_date": "2023-02-07",
"leave_date": "2025-06-27"
},
{
"emp_id": 37,
"name": "许华",
"department": "销售部",
"level": 8,
"hire_date": "2022-01-20",
"leave_date": null
},
{
"emp_id": 38,
"name": "金丽",
"department": "市场部",
"level": 6,
"hire_date": "2021-09-07",
"leave_date": null
},
{
"emp_id": 39,
"name": "吕芳",
"department": "销售部",
"level": 3,
"hire_date": "2022-11-15",
"leave_date": null
},
{
"emp_id": 40,
"name": "韩伟",
"department": "人力资源部",
"level": 4,
"hire_date": "2021-12-01",
"leave_date": null
}
]
@@ -0,0 +1,584 @@
{
"schema_version": "1.0",
"experiment": "5-10",
"run_id": "20260729T210334Z-5_10-postgresql",
"started_at_utc": "2026-07-29T21:03:34.066223+00:00",
"completed_at_utc": "2026-07-29T21:06:43.050217+00:00",
"provider": "ark",
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-250615",
"postgresql": {
"version": "PostgreSQL 14.13 (Homebrew) on aarch64-apple-darwin23.6.0, compiled by Apple clang version 16.0.0 (clang-1600.0.26.4), 64-bit",
"database": "postgres",
"schema": "exp5_10_20260729t210334z510postgresql",
"employees": 40,
"salary_rows": 1184
},
"source": {
"manuscript": "book/chapter5.md#实验-5-10",
"campaign_sha256": "d921a6d18683dcbcfa84f4ea08ad67a622315ddf54b05cf9a1ea048564eae209",
"seed_sha256": "833c1a09349802dfad70c84b7d568d1e06ec3d73bdef588c796b2d704a73730d"
},
"records": [
{
"id": 1,
"question": "平均每个员工在职多久?",
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"query_latency_s": 0.0015,
"execution_error": null
}
],
"rows": [
[
886.2
]
],
"row_count": 1,
"query_latency_s": 0.0015,
"expected": [
[
886.2
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 2,
"question": "每个部门有多少在职员工?",
"sql": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"query_latency_s": 0.0007,
"execution_error": null
}
],
"rows": [
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"研发部",
5
],
[
"财务部",
6
],
[
"销售部",
10
]
],
"row_count": 5,
"query_latency_s": 0.0007,
"expected": [
[
"研发部",
5
],
[
"销售部",
10
],
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"财务部",
6
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 3,
"question": "哪个部门员工平均级别最高?",
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"query_latency_s": 0.0008,
"execution_error": null
}
],
"rows": [
[
"销售部"
]
],
"row_count": 1,
"query_latency_s": 0.0008,
"expected": [
[
"销售部"
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 4,
"question": "每个部门今年和去年各新入职多少人?",
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0",
"query_latency_s": 0.0008,
"execution_error": null
}
],
"rows": [
[
"财务部",
0,
1
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"研发部",
1,
2
],
[
"市场部",
0,
4
]
],
"row_count": 5,
"query_latency_s": 0.0008,
"expected": [
[
"研发部",
1,
2
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"市场部",
0,
4
],
[
"财务部",
0,
1
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 5,
"question": "前年3月到去年5月,A部门平均工资是多少?",
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"query_latency_s": 0.0021,
"execution_error": null
}
],
"rows": [
[
27695.833333333332
]
],
"row_count": 1,
"query_latency_s": 0.0021,
"expected": [
[
27695.83
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 6,
"question": "去年A部门和B部门平均工资哪个高?",
"sql": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department",
"query_latency_s": 0.0011,
"execution_error": null
}
],
"rows": [
[
"研发部",
28569.444444444445
],
[
"销售部",
29481.55339805825
]
],
"row_count": 2,
"query_latency_s": 0.0011,
"expected": [
[
"研发部",
28569.44
],
[
"销售部",
29481.55
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 7,
"question": "今年每个级别的员工平均工资是多少?",
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"query_latency_s": 0.0014,
"execution_error": null
}
],
"rows": [
[
3,
23456.428571428572
],
[
4,
26248.823529411766
],
[
5,
34534.75
],
[
6,
28461.363636363636
],
[
7,
29935.0
],
[
8,
32242.69230769231
],
[
9,
34559.90566037736
]
],
"row_count": 7,
"query_latency_s": 0.0014,
"expected": [
[
3,
23456.43
],
[
8,
32242.69
],
[
6,
28461.36
],
[
4,
26248.82
],
[
9,
34559.91
],
[
5,
34534.75
],
[
7,
29935
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 8,
"question": "入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?",
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket",
"query_latency_s": 0.0015,
"execution_error": null
}
],
"rows": [
[
"一到两年",
27683.125
],
[
"两到三年",
31138.75
],
[
"入职一年内",
24053.75
]
],
"row_count": 3,
"query_latency_s": 0.0015,
"expected": [
[
"入职一年内",
24053.75
],
[
"一到两年",
27683.12
],
[
"两到三年",
31138.75
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 9,
"question": "去年到今年涨薪幅度最大的10位员工是谁?",
"sql": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"query_latency_s": 0.0018,
"execution_error": null
}
],
"rows": [
[
"韩平",
12000.0
],
[
"韩伟",
2200.0
],
[
"吕芳",
2155.0
],
[
"金丽",
2110.0
],
[
"许华",
2065.0
],
[
"陶松",
1975.0
],
[
"周雪",
1930.0
],
[
"吴敏",
1885.0
],
[
"张霞",
1840.0
],
[
"赵平",
1795.0
]
],
"row_count": 10,
"query_latency_s": 0.0018,
"expected": [
[
"韩平",
12000
],
[
"韩伟",
2200
],
[
"吕芳",
2155
],
[
"金丽",
2110
],
[
"许华",
2065
],
[
"陶松",
1975
],
[
"周雪",
1930
],
[
"吴敏",
1885
],
[
"张霞",
1840
],
[
"赵平",
1795
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 10,
"question": "有没有拖欠工资的情况(某个月还在职却没有发薪)?",
"sql": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL",
"query_latency_s": 0.0122,
"execution_error": null
}
],
"rows": [
[
17,
"2026-01"
]
],
"row_count": 1,
"query_latency_s": 0.0122,
"expected": [
[
17,
"2026-01"
]
],
"passed": true,
"comparison": "independent Python reference matched"
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"html": "results.html",
"screenshot": "results.png"
},
"usage": {
"calls": 10,
"prompt_tokens": 3154,
"completion_tokens": 8192,
"total_tokens": 11346,
"model_latency_s": 187.73,
"db_latency_s": 0.0239
},
"artifacts": {
"employees.json": {
"path": "employees.json",
"sha256": "7e5e237da09edb3db029e367a52569267241673a99e19d2ffddbb85a45cb59bd"
},
"salaries.json": {
"path": "salaries.json",
"sha256": "70a9dcd335d5c3e277eac92a4ae351f0f33c56ef9b2a526047cf60c98ed04228"
},
"schema.sql": {
"path": "schema.sql",
"sha256": "76a7d9f85d8bf3998729c40fa448e61e49ec24d47acba2f04580e5d2aeb27596"
},
"receipts.json": {
"path": "receipts.json",
"sha256": "0eb53eb1ac08d3b4091d02db1053e67078ac83e0f564ba8ae6f76fbc937e7a85"
},
"queries_and_results.json": {
"path": "queries_and_results.json",
"sha256": "a2be9fa715a15fe4290ee250f67a1c742ff8a3c3cff215f664a0d77b10ec8b40"
},
"results.html": {
"path": "results.html",
"sha256": "6099929004da8eb22e56f2557d3e167cd39ae2aee2b4a70d2fe61576d1980639"
},
"results.png": {
"path": "results.png",
"sha256": "9dccf840df361ad4031eb559672639876595f038f907e95987cc5c7ca550462c"
}
},
"acceptance_gates": {
"real_postgresql_server": true,
"exact_two_table_schema_created": true,
"all_10_natural_language_questions_attempted": true,
"all_10_sql_artifacts_are_read_only": true,
"database_not_llm_received_rows": true,
"database_executed_every_artifact": true,
"all_10_answers_match_independent_reference": true,
"result_tables_rendered_directly_in_real_browser": true,
"raw_model_receipts_complete": true,
"repairs_use_execution_errors_only": true,
"raw_database_rows_and_hashes_retained": true
},
"official_complete": true
}
@@ -0,0 +1,504 @@
[
{
"id": 1,
"question": "平均每个员工在职多久?",
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees",
"query_latency_s": 0.0015,
"execution_error": null
}
],
"rows": [
[
886.2
]
],
"row_count": 1,
"query_latency_s": 0.0015,
"expected": [
[
886.2
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 2,
"question": "每个部门有多少在职员工?",
"sql": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department",
"query_latency_s": 0.0007,
"execution_error": null
}
],
"rows": [
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"研发部",
5
],
[
"财务部",
6
],
[
"销售部",
10
]
],
"row_count": 5,
"query_latency_s": 0.0007,
"expected": [
[
"研发部",
5
],
[
"销售部",
10
],
[
"人力资源部",
4
],
[
"市场部",
9
],
[
"财务部",
6
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 3,
"question": "哪个部门员工平均级别最高?",
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1",
"query_latency_s": 0.0008,
"execution_error": null
}
],
"rows": [
[
"销售部"
]
],
"row_count": 1,
"query_latency_s": 0.0008,
"expected": [
[
"销售部"
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 4,
"question": "每个部门今年和去年各新入职多少人?",
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0",
"query_latency_s": 0.0008,
"execution_error": null
}
],
"rows": [
[
"财务部",
0,
1
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"研发部",
1,
2
],
[
"市场部",
0,
4
]
],
"row_count": 5,
"query_latency_s": 0.0008,
"expected": [
[
"研发部",
1,
2
],
[
"销售部",
2,
1
],
[
"人力资源部",
2,
0
],
[
"市场部",
0,
4
],
[
"财务部",
0,
1
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 5,
"question": "前年3月到去年5月,A部门平均工资是多少?",
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)",
"query_latency_s": 0.0021,
"execution_error": null
}
],
"rows": [
[
27695.833333333332
]
],
"row_count": 1,
"query_latency_s": 0.0021,
"expected": [
[
27695.83
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 6,
"question": "去年A部门和B部门平均工资哪个高?",
"sql": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department",
"query_latency_s": 0.0011,
"execution_error": null
}
],
"rows": [
[
"研发部",
28569.444444444445
],
[
"销售部",
29481.55339805825
]
],
"row_count": 2,
"query_latency_s": 0.0011,
"expected": [
[
"研发部",
28569.44
],
[
"销售部",
29481.55
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 7,
"question": "今年每个级别的员工平均工资是多少?",
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"sql_attempts": [
{
"attempt": 1,
"sql": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level",
"query_latency_s": 0.0014,
"execution_error": null
}
],
"rows": [
[
3,
23456.428571428572
],
[
4,
26248.823529411766
],
[
5,
34534.75
],
[
6,
28461.363636363636
],
[
7,
29935.0
],
[
8,
32242.69230769231
],
[
9,
34559.90566037736
]
],
"row_count": 7,
"query_latency_s": 0.0014,
"expected": [
[
3,
23456.43
],
[
8,
32242.69
],
[
6,
28461.36
],
[
4,
26248.82
],
[
9,
34559.91
],
[
5,
34534.75
],
[
7,
29935
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 8,
"question": "入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?",
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket",
"query_latency_s": 0.0015,
"execution_error": null
}
],
"rows": [
[
"一到两年",
27683.125
],
[
"两到三年",
31138.75
],
[
"入职一年内",
24053.75
]
],
"row_count": 3,
"query_latency_s": 0.0015,
"expected": [
[
"入职一年内",
24053.75
],
[
"一到两年",
27683.12
],
[
"两到三年",
31138.75
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 9,
"question": "去年到今年涨薪幅度最大的10位员工是谁?",
"sql": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10",
"query_latency_s": 0.0018,
"execution_error": null
}
],
"rows": [
[
"韩平",
12000.0
],
[
"韩伟",
2200.0
],
[
"吕芳",
2155.0
],
[
"金丽",
2110.0
],
[
"许华",
2065.0
],
[
"陶松",
1975.0
],
[
"周雪",
1930.0
],
[
"吴敏",
1885.0
],
[
"张霞",
1840.0
],
[
"赵平",
1795.0
]
],
"row_count": 10,
"query_latency_s": 0.0018,
"expected": [
[
"韩平",
12000
],
[
"韩伟",
2200
],
[
"吕芳",
2155
],
[
"金丽",
2110
],
[
"许华",
2065
],
[
"陶松",
1975
],
[
"周雪",
1930
],
[
"吴敏",
1885
],
[
"张霞",
1840
],
[
"赵平",
1795
]
],
"passed": true,
"comparison": "independent Python reference matched"
},
{
"id": 10,
"question": "有没有拖欠工资的情况(某个月还在职却没有发薪)?",
"sql": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL",
"sql_attempts": [
{
"attempt": 1,
"sql": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL",
"query_latency_s": 0.0122,
"execution_error": null
}
],
"rows": [
[
17,
"2026-01"
]
],
"row_count": 1,
"query_latency_s": 0.0122,
"expected": [
[
17,
"2026-01"
]
],
"passed": true,
"comparison": "independent Python reference matched"
}
]
@@ -0,0 +1,332 @@
[
{
"question_id": 1,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:03:45.098657+00:00",
"latency_s": 10.894,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 平均每个员工在职多久?\nPostgreSQL guidance: Use COALESCE(leave_date, CURRENT_DATE) - hire_date to obtain integer days, then AVG. Return one numeric column."
}
],
"temperature": 0
},
"response": {
"id": "021785359014553b2d574b8a133720ef9fae8eba78adee9444980",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees;"
},
"usage": {
"prompt_tokens": 299,
"completion_tokens": 500,
"total_tokens": 799,
"cached_prompt_tokens": 0
}
},
{
"question_id": 2,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:03:55.131184+00:00",
"latency_s": 10.031,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 每个部门有多少在职员工?\nPostgreSQL guidance: Active means leave_date IS NULL. GROUP BY department. Return department and active count."
}
],
"temperature": 0
},
"response": {
"id": "021785359025290b2d574b8a133720ef9fae8eba78adee91ab80e",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department;"
},
"usage": {
"prompt_tokens": 288,
"completion_tokens": 379,
"total_tokens": 667,
"cached_prompt_tokens": 0
}
},
{
"question_id": 3,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:04:02.665976+00:00",
"latency_s": 7.534,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 哪个部门员工平均级别最高?\nPostgreSQL guidance: Average level across all employees by department; ORDER BY the average descending and LIMIT 1. Return department only."
}
],
"temperature": 0
},
"response": {
"id": "021785359035323b2d574b8a133720ef9fae8eba78adee9cba0b8",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department\nFROM employees\nGROUP BY department\nORDER BY AVG(level) DESC\nLIMIT 1;"
},
"usage": {
"prompt_tokens": 293,
"completion_tokens": 371,
"total_tokens": 664,
"cached_prompt_tokens": 0
}
},
{
"question_id": 4,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:04:20.434412+00:00",
"latency_s": 17.767,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 每个部门今年和去年各新入职多少人?\nPostgreSQL guidance: Use COUNT(*) FILTER with EXTRACT(YEAR FROM hire_date) for current and previous years. Return department, this-year count, last-year count; omit departments with both zero."
}
],
"temperature": 0
},
"response": {
"id": "021785359042857b2d574b8a133720ef9fae8eba78adee9e96329",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department,\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS \"this-year count\",\n COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS \"last-year count\"\nFROM employees\nGROUP BY department\nHAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) > 0\n OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) > 0;"
},
"usage": {
"prompt_tokens": 312,
"completion_tokens": 749,
"total_tokens": 1061,
"cached_prompt_tokens": 0
}
},
{
"question_id": 5,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:04:48.475419+00:00",
"latency_s": 28.04,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 前年3月到去年5月,A部门平均工资是多少?\nPostgreSQL guidance: A=研发部. Inclusive dates are March 1 two years ago through May 31 last year; derive years from CURRENT_DATE with make_date, never literals. Return AVG(salary)."
}
],
"temperature": 0
},
"response": {
"id": "021785359060627b2d574b8a133720ef9fae8eba78adee9eb2c59",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT AVG(s.salary)\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE e.department = '研发部'\n AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)\n AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31);"
},
"usage": {
"prompt_tokens": 322,
"completion_tokens": 1337,
"total_tokens": 1659,
"cached_prompt_tokens": 0
}
},
{
"question_id": 6,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:05:15.145844+00:00",
"latency_s": 26.668,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 去年A部门和B部门平均工资哪个高?\nPostgreSQL guidance: A=研发部 and B=销售部. Join employees to salaries; filter pay_date to previous calendar year, group by department, and return department plus average salary for exactly those two departments."
}
],
"temperature": 0
},
"response": {
"id": "021785359088668b2d574b8a133720ef9fae8eba78adee96ff8e8",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT department, AVG(salary) AS average_salary\nFROM employees\nJOIN salaries ON employees.emp_id = salaries.emp_id\nWHERE department IN ('研发部', '销售部')\n AND pay_date >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'\n AND pay_date < date_trunc('year', CURRENT_DATE)\nGROUP BY department;"
},
"usage": {
"prompt_tokens": 314,
"completion_tokens": 1093,
"total_tokens": 1407,
"cached_prompt_tokens": 0
}
},
{
"question_id": 7,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:05:25.677375+00:00",
"latency_s": 10.53,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 今年每个级别的员工平均工资是多少?\nPostgreSQL guidance: Join salary rows to employees, filter pay_date to current calendar year, group by level. Return level and average salary."
}
],
"temperature": 0
},
"response": {
"id": "021785359115339b2d574b8a133720ef9fae8eba78adee94ab7c5",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "SELECT e.level, AVG(s.salary) AS average_salary\nFROM employees e\nJOIN salaries s ON e.emp_id = s.emp_id\nWHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)\nGROUP BY e.level;"
},
"usage": {
"prompt_tokens": 298,
"completion_tokens": 408,
"total_tokens": 706,
"cached_prompt_tokens": 0
}
},
{
"question_id": 8,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:05:54.537092+00:00",
"latency_s": 28.857,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?\nPostgreSQL guidance: First select each employee's latest salary with DISTINCT ON (emp_id) ordered by pay_date DESC. Bucket CURRENT_DATE-hire_date as <365 入职一年内, 365..729 一到两年, 730..1094 两到三年; exclude older. Return bucket and average latest salary."
}
],
"temperature": 0
},
"response": {
"id": "021785359125871b2d574b8a133720ef9fae8eba78adee9b33ba5",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "WITH latest_salaries AS (\n SELECT DISTINCT ON (emp_id) emp_id, salary\n FROM salaries\n ORDER BY emp_id, pay_date DESC\n)\nSELECT \n CASE \n WHEN (CURRENT_DATE - e.hire_date)::INTEGER < 365 THEN '入职一年内'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN '一到两年'\n WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN '两到三年'\n END AS bucket,\n AVG(ls.salary) AS average_latest_salary\nFROM employees e\nJOIN latest_salaries ls ON e.emp_id = ls.emp_id\nWHERE e.leave_date IS NULL\n AND (CURRENT_DATE - e.hire_date)::INTEGER < 1095\nGROUP BY bucket\nORDER BY bucket;"
},
"usage": {
"prompt_tokens": 360,
"completion_tokens": 1364,
"total_tokens": 1724,
"cached_prompt_tokens": 0
}
},
{
"question_id": 9,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:06:19.333907+00:00",
"latency_s": 24.794,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 去年到今年涨薪幅度最大的10位员工是谁?\nPostgreSQL guidance: Aggregate each employee's average salary separately for current and previous calendar years with FILTER, keep employees having both, compute current minus previous, order descending, LIMIT 10. Return name and raise amount."
}
],
"temperature": 0
},
"response": {
"id": "021785359154731b2d574b8a133720ef9fae8eba78adee94f6e87",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "WITH emp_salary_avg AS (\n SELECT \n emp_id,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,\n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg\n FROM salaries\n GROUP BY emp_id\n HAVING \n AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL\n AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL\n)\nSELECT \n e.name,\n (esa.current_avg - esa.previous_avg) AS raise_amount\nFROM emp_salary_avg esa\nJOIN employees e ON esa.emp_id = e.emp_id\nORDER BY raise_amount DESC\nLIMIT 10;"
},
"usage": {
"prompt_tokens": 320,
"completion_tokens": 1101,
"total_tokens": 1421,
"cached_prompt_tokens": 0
}
},
{
"question_id": 10,
"purpose": "initial_sql_generation",
"attempt": 1,
"called_at_utc": "2026-07-29T21:06:41.951264+00:00",
"latency_s": 22.615,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "You are an ERP natural-language-to-SQL Agent. Output exactly one read-only\nPostgreSQL SELECT statement (WITH/CTE is allowed), with no Markdown or prose.\n\nSchema:\nemployees(emp_id INTEGER PRIMARY KEY, name TEXT, department TEXT, level INTEGER,\n hire_date DATE, leave_date DATE NULL)\nsalaries(emp_id INTEGER REFERENCES employees, pay_date DATE, salary INTEGER,\n PRIMARY KEY(emp_id,pay_date))\n\nBusiness meanings: leave_date NULL means active; A department is 研发部; B is 销售部.\nUse CURRENT_DATE for all relative dates. Never hard-code a calendar year. Follow the\nrequested output columns exactly. You write only the SQL artifact: you do not see,\ncopy, summarize, or calculate over result rows."
},
{
"role": "user",
"content": "Question: 有没有拖欠工资的情况(某个月还在职却没有发薪)?\nPostgreSQL guidance: For every employee generate each employed month with LATERAL generate_series(date_trunc('month', hire_date), date_trunc('month', COALESCE(leave_date,CURRENT_DATE)), interval '1 month'); left join salaries by emp_id and month. Return missing emp_id and to_char(month,'YYYY-MM')."
}
],
"temperature": 0
},
"response": {
"id": "021785359179526b2d574b8a133720ef9fae8eba78adee94328f6",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "WITH employee_employed_months AS (\n SELECT \n e.emp_id,\n generate_series(\n date_trunc('month', e.hire_date),\n date_trunc('month', COALESCE(e.leave_date, CURRENT_DATE)),\n interval '1 month'\n ) AS employed_month\n FROM employees e\n)\nSELECT \n em.emp_id,\n to_char(em.employed_month, 'YYYY-MM') AS month\nFROM employee_employed_months em\nLEFT JOIN salaries s \n ON em.emp_id = s.emp_id \n AND date_trunc('month', s.pay_date) = em.employed_month\nWHERE s.emp_id IS NULL;"
},
"usage": {
"prompt_tokens": 348,
"completion_tokens": 890,
"total_tokens": 1238,
"cached_prompt_tokens": 0
}
}
]
@@ -0,0 +1,79 @@
<!doctype html><meta charset=utf-8><title>Experiment 5-10 PostgreSQL artifacts</title>
<style>body{font-family:system-ui;margin:30px;background:#f7f8fa;color:#172033}section{background:white;padding:18px;margin:16px 0;border-radius:12px}table{border-collapse:collapse}td{border:1px solid #ccd3dd;padding:5px 9px}pre{white-space:pre-wrap;background:#eef2f7;padding:12px}.ok{color:#08783e}.bad{color:#b42318}</style>
<h1>ERP Agent: SQL artifacts executed by PostgreSQL</h1><section><h2>1. 平均每个员工在职多久?</h2><pre>SELECT AVG(COALESCE(leave_date, CURRENT_DATE) - hire_date) AS average_tenure_days FROM employees</pre><table><tr><td>886.2</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>2. 每个部门有多少在职员工?</h2><pre>SELECT department, COUNT(emp_id) AS active_count FROM employees WHERE leave_date IS NULL GROUP BY department</pre><table><tr><td>人力资源部</td><td>4</td></tr><tr><td>市场部</td><td>9</td></tr><tr><td>研发部</td><td>5</td></tr><tr><td>财务部</td><td>6</td></tr><tr><td>销售部</td><td>10</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>3. 哪个部门员工平均级别最高?</h2><pre>SELECT department
FROM employees
GROUP BY department
ORDER BY AVG(level) DESC
LIMIT 1</pre><table><tr><td>销售部</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>4. 每个部门今年和去年各新入职多少人?</h2><pre>SELECT department,
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS &quot;this-year count&quot;,
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS &quot;last-year count&quot;
FROM employees
GROUP BY department
HAVING COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE)) &gt; 0
OR COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM hire_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) &gt; 0</pre><table><tr><td>财务部</td><td>0</td><td>1</td></tr><tr><td>销售部</td><td>2</td><td>1</td></tr><tr><td>人力资源部</td><td>2</td><td>0</td></tr><tr><td>研发部</td><td>1</td><td>2</td></tr><tr><td>市场部</td><td>0</td><td>4</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>5. 前年3月到去年5月,A部门平均工资是多少?</h2><pre>SELECT AVG(s.salary)
FROM employees e
JOIN salaries s ON e.emp_id = s.emp_id
WHERE e.department = &#x27;研发部&#x27;
AND s.pay_date BETWEEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2, 3, 1)
AND make_date(EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1, 5, 31)</pre><table><tr><td>27695.833333333332</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>6. 去年A部门和B部门平均工资哪个高?</h2><pre>SELECT department, AVG(salary) AS average_salary
FROM employees
JOIN salaries ON employees.emp_id = salaries.emp_id
WHERE department IN (&#x27;研发部&#x27;, &#x27;销售部&#x27;)
AND pay_date &gt;= date_trunc(&#x27;year&#x27;, CURRENT_DATE) - INTERVAL &#x27;1 year&#x27;
AND pay_date &lt; date_trunc(&#x27;year&#x27;, CURRENT_DATE)
GROUP BY department</pre><table><tr><td>研发部</td><td>28569.444444444445</td></tr><tr><td>销售部</td><td>29481.55339805825</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>7. 今年每个级别的员工平均工资是多少?</h2><pre>SELECT e.level, AVG(s.salary) AS average_salary
FROM employees e
JOIN salaries s ON e.emp_id = s.emp_id
WHERE EXTRACT(YEAR FROM s.pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)
GROUP BY e.level</pre><table><tr><td>3</td><td>23456.428571428572</td></tr><tr><td>4</td><td>26248.823529411766</td></tr><tr><td>5</td><td>34534.75</td></tr><tr><td>6</td><td>28461.363636363636</td></tr><tr><td>7</td><td>29935.0</td></tr><tr><td>8</td><td>32242.69230769231</td></tr><tr><td>9</td><td>34559.90566037736</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>8. 入职一年内、一到两年、两到三年的员工,最近一个月平均工资是多少?</h2><pre>WITH latest_salaries AS (
SELECT DISTINCT ON (emp_id) emp_id, salary
FROM salaries
ORDER BY emp_id, pay_date DESC
)
SELECT
CASE
WHEN (CURRENT_DATE - e.hire_date)::INTEGER &lt; 365 THEN &#x27;入职一年内&#x27;
WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 365 AND 729 THEN &#x27;一到两年&#x27;
WHEN (CURRENT_DATE - e.hire_date)::INTEGER BETWEEN 730 AND 1094 THEN &#x27;两到三年&#x27;
END AS bucket,
AVG(ls.salary) AS average_latest_salary
FROM employees e
JOIN latest_salaries ls ON e.emp_id = ls.emp_id
WHERE e.leave_date IS NULL
AND (CURRENT_DATE - e.hire_date)::INTEGER &lt; 1095
GROUP BY bucket
ORDER BY bucket</pre><table><tr><td>一到两年</td><td>27683.125</td></tr><tr><td>两到三年</td><td>31138.75</td></tr><tr><td>入职一年内</td><td>24053.75</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>9. 去年到今年涨薪幅度最大的10位员工是谁?</h2><pre>WITH emp_salary_avg AS (
SELECT
emp_id,
AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) AS current_avg,
AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS previous_avg
FROM salaries
GROUP BY emp_id
HAVING
AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE)) IS NOT NULL
AND AVG(salary) FILTER (WHERE EXTRACT(YEAR FROM pay_date) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) IS NOT NULL
)
SELECT
e.name,
(esa.current_avg - esa.previous_avg) AS raise_amount
FROM emp_salary_avg esa
JOIN employees e ON esa.emp_id = e.emp_id
ORDER BY raise_amount DESC
LIMIT 10</pre><table><tr><td>韩平</td><td>12000.0</td></tr><tr><td>韩伟</td><td>2200.0</td></tr><tr><td>吕芳</td><td>2155.0</td></tr><tr><td>金丽</td><td>2110.0</td></tr><tr><td>许华</td><td>2065.0</td></tr><tr><td>陶松</td><td>1975.0</td></tr><tr><td>周雪</td><td>1930.0</td></tr><tr><td>吴敏</td><td>1885.0</td></tr><tr><td>张霞</td><td>1840.0</td></tr><tr><td>赵平</td><td>1795.0</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section><section><h2>10. 有没有拖欠工资的情况(某个月还在职却没有发薪)?</h2><pre>WITH employee_employed_months AS (
SELECT
e.emp_id,
generate_series(
date_trunc(&#x27;month&#x27;, e.hire_date),
date_trunc(&#x27;month&#x27;, COALESCE(e.leave_date, CURRENT_DATE)),
interval &#x27;1 month&#x27;
) AS employed_month
FROM employees e
)
SELECT
em.emp_id,
to_char(em.employed_month, &#x27;YYYY-MM&#x27;) AS month
FROM employee_employed_months em
LEFT JOIN salaries s
ON em.emp_id = s.emp_id
AND date_trunc(&#x27;month&#x27;, s.pay_date) = em.employed_month
WHERE s.emp_id IS NULL</pre><table><tr><td>17</td><td>2026-01</td></tr></table><p class=ok>PASS: independent Python reference matched</p></section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

@@ -0,0 +1,34 @@
-- 实验 5-10 ERP Agent —— 书中要求的 PostgreSQL schema(两张表)。
--
-- 本仓库的可运行演示用 SQLite(零依赖、可离线复现,见 seed.py / demo.py);
-- 这份 DDL 给出书中原文的 PostgreSQL 版本,方便迁移到真实 Postgres 环境。
-- 两种方言的表结构一致,差异主要在日期函数:
-- SQLite: strftime('%Y','now') julianday(a)-julianday(b) date('now','-1 year')
-- PostgreSQL: EXTRACT(YEAR FROM now()) (a::date - b::date) now() - interval '1 year'
--
-- 用法(需本机有 PostgreSQL):
-- createdb erp
-- psql erp -f schema_postgres.sql
DROP TABLE IF EXISTS salaries;
DROP TABLE IF EXISTS employees;
-- 员工表:ID、姓名、部门、级别(数字越大越高)、入职日期、离职日期(NULL = 在职)
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT NOT NULL,
level INTEGER NOT NULL,
hire_date DATE NOT NULL,
leave_date DATE -- NULL 表示在职
);
-- 工资表:员工ID、发薪日期(每月一条,取当月 1 号)、当月工资
CREATE TABLE salaries (
emp_id INTEGER NOT NULL REFERENCES employees(emp_id),
pay_date DATE NOT NULL, -- 每月一条,如 2025-03-01
salary INTEGER NOT NULL,
PRIMARY KEY (emp_id, pay_date)
);
CREATE INDEX idx_salaries_pay_date ON salaries (pay_date);