06 · 工具调用:Text2SQL 与四层护栏

对应代码:app/agent/tools.pyapp/agent/guardrails.py

一、工具即 Schema

python 复制代码
Tool(
    name="sql_query",
    description="BI 数据查询(Text2SQL):项目成本、缺陷趋势、交付效率等指标统计。只读。",
    parameters={"type": "object", "properties": {
        "question": {"type": "string", "description": "要查询的业务问题,中文描述即可"}},
        "required": ["question"]},
    fn=_tool_sql_query,
    risk="low",        # low | high
)

注册表对外提供三个能力:

python 复制代码
registry.specs()                  # JSON Schema 清单(给 API / 前端)
registry.describe_for_prompt()    # 自然语言清单(自动塞进 Plan 的 Prompt)
registry.execute(name, args, trace_id)   # 统一执行入口

收益:新增工具只改一处,编排层零改动。

二、目前注册的 5 个工具

工具 风险 说明
search_knowledge low 知识库检索(RAG)
sql_query low BI 数据 Text2SQL
send_alert high 推送告警(需人工确认)
calculator low 安全四则运算
get_today low 解析"本月/上周"相对时间

calculator 用 AST 白名单实现(绝不用 eval):

python 复制代码
_ALLOWED_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ...}
def _safe_eval(expr):
    return _eval(ast.parse(expr, mode="eval"))

三、Text2SQL:生成 → 校验 → 执行 → 报错回炉

python 复制代码
for attempt in range(2):
    prompt = TEXT2SQL_TMPL.format(schema=schema, question=question, limit=row_limit)
    if last_err:
        prompt += f"\n上一次生成的 SQL 报错:{last_err}\n请修正后只输出 SQL。"
    raw_sql = llm.chat([{"role": "user", "content": prompt}], temperature=0)
    try:
        safe_sql = validate_sql(raw_sql)       # ← 护栏
        return execute(safe_sql)
    except GuardrailError as e:
        last_err = str(e)                      # 护栏拦截也算一次错误,回炉重生成

踩坑记录: 第一版没有把数据库报错回传,模型生成的 SQL 字段名写错就永远失败; 加上"报错回炉"后,一次修正成功率显著提升。

Prompt 里的关键约束:

sql 复制代码
1. 只能输出一条 SELECT,不要解释、不要 markdown 代码块
2. 禁止 INSERT/UPDATE/DELETE/DROP/ATTACH/PRAGMA
3. 必须带 LIMIT 200
4. 日期字段为 TEXT(YYYY-MM-DD),本月用 date('now','start of month')
5. 只能用给定的表和字段
+ 一个少样本示例

四、四层 SQL 护栏

python 复制代码
def validate_sql(sql, allowed_tables, row_limit, readonly=True):
    # ① 清洗:去 markdown 代码块、去注释
    clean = strip_sql_comments(raw)
    # ② 语法层:禁多语句
    if _MULTI_STMT.search(clean): raise GuardrailError("检测到多语句执行")
    # ③ 关键字层:必须以 SELECT/WITH 开头
    if readonly and not low.startswith(("select", "with")): raise ...
    #    危险关键字黑名单
    for kw in ("insert","update","delete","drop","attach","pragma",...):
        if re.search(rf"\b{kw}\b", low): raise ...
    # ④ 权限层:正则提取 from/join 后的表名,必须在白名单
    tables = set(_TABLE_RE.findall(clean))
    if illegal := tables - allowed: raise GuardrailError(f"引用了未授权的表:{illegal}")
    # ⑤ 兜底:没 LIMIT 自动补
    if not re.search(r"\blimit\s+\d+", low): clean += f" LIMIT {row_limit}"

再加一层数据库侧保护:只读连接

python 复制代码
con = sqlite3.connect(f"file:{db_path}?mode=ro")   # SQLite 只读模式

(生产换成只读账号)

实测拦截效果

输入 结果
"删掉成本表" ✅ 被关键字黑名单拦截
"SELECT * FROM secret_salary" ✅ 被表白名单拦截
"SELECT ...; DROP TABLE x" ✅ 被多语句检测拦截

五、Human-in-the-loop

python 复制代码
if tool.risk == "high" and settings.hitl_enabled and not auto_approve_high_risk:
    return ToolResult(False, error="HITL_REQUIRED", meta={"need_human": True})

前端收到 hitl 事件 → 渲染橙色确认卡 → 用户点「确认执行」→ 带 approved=true 重新请求 → 后端从 _pending[thread_id] 取出并执行。

这就是 JD 里说的"权限边界"。

六、另外两个护栏

提示词注入检测

python 复制代码
INJECTION_PATTERNS = [
    r"忽略(?:以上|上面|之前).{0,6}(?:指令|规则|提示)",
    r"ignore\s+(?:all\s+)?previous\s+instructions",
    r"输出(?:你的|系统).{0,4}(?:提示词|prompt)",
    ...
]

命中后不中断服务,而是降级为"只读检索模式"并发一条 warn 事件------ 粗暴拒绝会伤害正常用户体验。

PII 脱敏(出网前)

python 复制代码
_PII_RULES = [
    (re.compile(r"1[3-9]\d{9}"), "[PHONE]"),          # 手机号
    (re.compile(r"\b\d{17}[\dXx]\b"), "[ID_CARD]"),   # 身份证
    (re.compile(r"\b\d{16,19}\b"), "[CARD]"),         # 银行卡
    (re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"), "[EMAIL]"),
    (re.compile(r"(?:sk-|api[_-]?key\s*[:=]\s*)[A-Za-z0-9_\-]{16,}"), "[SECRET]"),
]

为什么必须做:企业场景用户会随手粘贴客户手机号、身份证号。 一旦发到第三方模型,就是合规事故。

七、工具执行的可观测

每个工具调用都会被 Tracer 包成一个 Span:

python 复制代码
with tracer.span(trace_id, f"tool.{name}", args=args, risk=tool.risk):
    result = tool.fn(args, trace_id)

同时推送 tool_start / tool_end 事件给前端, 所以你能在执行链路时间线上看到每一步工具调用的参数、耗时和结果预览


下一篇:07 · 记忆、可观测与成本控制

相关推荐
再吃一根胡萝卜1 小时前
02 · 项目总览与 5 分钟跑起来
面试
再吃一根胡萝卜1 小时前
10 · 踩坑记录与优化清单
面试
再吃一根胡萝卜1 小时前
09 · Java 双栈:Spring Boot + LangChain4j
面试
再吃一根胡萝卜1 小时前
07 · 记忆、可观测与成本控制
面试
再吃一根胡萝卜1 小时前
04 · RAG 全流程:从切分到引用溯源
面试
再吃一根胡萝卜1 小时前
11 · 如何把项目写进简历与面试
面试
再吃一根胡萝卜7 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜7 小时前
03 · 后端:FastAPI 与分层架构
面试
Interview Aid11212 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展