ARTICLE · 1213760
文档答得了,数还得查:用 SQL 工具把 Agent 接到业务库
昨天把 Agent 评测跑通了:Tracing 看过程,通过率定结果。可合同审核现场还有一类问题——账期、责任上限、订单状态,往往不在 PDF 政策里,而在业务库的表里。文档答得了「该怎么审」,数还得查「这笔合同实际写了什么」。这一篇开始接数据。
一、文档能答政策,表里才有这笔单

前面的 RAG 吃的是制度、模板、审核指南;问「账期一般怎么约定」,文档够用。可客户丢过来一份合同编号,要的是这笔单的 payment_terms、liability_cap——那是库里一行行的业务数据,不是政策摘要。
FDE 交付里这两类都要:政策走文档检索,订单、合同、库存走库表查询。只接一边,Agent 要么只会背规章,要么只会报数字、说不清依据。合同审核 Agent 要同时摸到两边,才像现场同事。
二、两种接法:自由 SELECT 与预置参数查询

接库时我对比过两条路。
一条是预置参数化查询:工具写成 get_contract_by_id(contract_id),SQL 写死在代码里,模型只填编号。可控——语句形状固定,不好乱写;但灵活差——多一个筛选条件就要加工具、改 schema。
另一条是让模型自己写 SELECT:工具是 run_select(sql),Agent 按问题拼查询。灵活——临时条件、联表都能试;但可控性要靠外围兜:只读账号、禁 DML、强制 LIMIT、事后脱敏。
我交付里选的是后者:模型写 SELECT,底座用只读账号 + 审计。演示里两种工具都会留着,方便对照;生产替换假 query_payment_terms 时,走的是自由 SELECT 这条。
三、安全底线:只读、禁写、LIMIT、查后再脱敏

给模型写 SQL 的权限,不等于给它改库的权限。我固定四条底线:
- 只读账号
:连库用 SELECT-only 角色,应用层拦不住时,库侧也拦得住 - 拒绝非 SELECT
:解析首词, INSERT/UPDATE/DELETE/DROP直接拒 - 强制 LIMIT
:没写就补上,避免一次扫全表 - 敏感列查后再脱敏
:不把列从模型眼前藏掉——藏了它会乱猜;查出来再 redact,日志里也留脱敏后的摘要
「先查后脱敏」比「不给模型看列」更稳:模型知道有 liability_cap,但返回下游或写进工单前,金额可以打码。权限边界在工具出口,不在 schema 谎言里。
四、接到现有 Agent:用真查询换掉假 query_payment_terms

合同 Agent 里原来的 query_payment_terms 是假工具,返回写死的字符串。现在换成对着演示库的真实查询,仍由模型写 SELECT。最小演示用 SQLite:
import re, sqlite3conn = sqlite3.connect("demo_contracts.db")conn.execute("CREATE TABLE IF NOT EXISTS contracts(" "contract_id TEXT PRIMARY KEY, payment_terms TEXT, liability_cap REAL)")conn.executemany("INSERT OR REPLACE INTO contracts VALUES (?,?,?)", [("C-1001", "Net 30", 500000.0), ("C-1002", "Net 60", 200000.0)])conn.commit()SENSITIVE = {"liability_cap"}def redact(rows, cols): return [{c: ("***" if c in SENSITIVE else v) for c, v in zip(cols, r)} for r in rows]def get_contract_by_id(contract_id: str) -> list[dict]: """参数化:SQL 写死,模型只填编号。""" cur = conn.execute( "SELECT contract_id, payment_terms, liability_cap FROM contracts WHERE contract_id = ?", (contract_id,)) cols = [d[0] for d in cur.description] return redact(cur.fetchall(), cols)def run_select(sql: str) -> list[dict]: """自由 SELECT:模型写语句;只读检查 + LIMIT + 脱敏。""" s = sql.strip().rstrip(";") if not re.match(r"(?is)^select\b", s): raise ValueError("only SELECT allowed") if re.search(r"(?is)\b(insert|update|delete|drop|alter|attach)\b", s): raise ValueError("DML/DDL blocked") if not re.search(r"(?is)\blimit\s+\d+", s): s = f"{s} LIMIT 50" cur = conn.execute(s) # 生产换只读账号连接 cols = [d[0] for d in cur.description] return redact(cur.fetchall(), cols)# Agent 侧用 run_select 替换假 query_payment_termsget_contract_by_id 适合对照可控性;线上我让 Agent 调 run_select,提示词里写清表结构与禁止事项。假工具一换掉,评测集里原来的期望序列也要改成新工具名——昨天那套通过率脚本继续用。
五、交付时客户会问:谁能查、查了什么、会不会改库

接业务库,客户很少先问模型聪不聪明,而是问三句:
- 谁能查
:工具挂在哪个角色、哪些会话能调 run_select,要和现有权限模型对齐 - 查了什么
:每条 SQL、调用人、时间进审计(可跟 JSONL Tracing 同一条时间线) - 会不会改库
:只读账号 + 应用层拒 DML,两道闸都要能演示
这三句答得清,SQL 工具才算可交付;答不清,灵活反而变成风险。我演示时会故意丢一条 UPDATE,让客户看见拒绝日志,再翻一条脱敏后的查询摘要——比口头保证管用。
文档解决「该怎么做」,库表解决「这笔是什么」;只读、脱敏、审计把「敢接」变成「能交」。
下一步:把同一套工具接到企业系统 / API(订单、CRM),不只停在演示库。
参考资料
[1] LangChain SQL agent:https://docs.langchain.com/oss/python/langchain/sql-agent
[2] OpenAI GPT Actions SQL database:https://developers.openai.com/cookbook/examples/chatgpt/gpt_actions_library/gpt_action_sql_database/
— 夏天软件AI —















































































