夜雨聆风学习资料网

ARTICLE · 1213760

文档答得了,数还得查:用 SQL 工具把 Agent 接到业务库

文档答得了,数还得查:用 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 的权限,不等于给它改库的权限。我固定四条底线:

  1. 只读账号
    :连库用 SELECT-only 角色,应用层拦不住时,库侧也拦得住
  2. 拒绝非 SELECT
    :解析首词,INSERT/UPDATE/DELETE/DROP 直接拒
  3. 强制 LIMIT
    :没写就补上,避免一次扫全表
  4. 敏感列查后再脱敏
    :不把列从模型眼前藏掉——藏了它会乱猜;查出来再 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_terms

get_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 —

相关学习资料