让数据 Agent 有证据才回答
用约 60 行 Python 构建 LangChain 数据 Agent,让工具返回带查询和数据行的答案,或明确的拒答状态。示例采用 FactQL,核心模式可迁移到其他数据工具。
AI 深度解读
数据库工具返回空结果,AI 却给出一个看起来合理的营收数字,整个调用链还没有报错。这篇文章要补上的,就是这个容易被忽略的缺口:数据工具除了返回答案,还要明确告诉 agent「无法核实」,并规定模型收到这种结果后必须停在哪里。
作者用约 60 行 Python、LangChain v1 和 FactQL 演示了这套做法。值得带走的是工具与模型之间的约定:答案附带证据,拒答有明确标记,系统提示禁止补数字。但它能约束到什么程度,还得分开看。
HTTP 200 为什么也可能需要拒答
作者描述的问题很具体:用户问上季度营收,工具没有找到足够的数据,模型却把缺口补成一句带数字的回答。接口没有异常,用户也很难察觉。
FactQL 的处理方式是把「请求正常完成」和「数据足以回答」分开。无法支持答案时,它仍返回 HTTP 200,但响应里是 "result": "error",并通过 decline_reason 和 explanation 说明原因。原文给出的拒答原因是 no_safe_sql,解释会建议用户明确表名或列名。
因此,只调用 raise_for_status() 不够。它能发现 HTTP 错误,不能判断业务数据是否支持结论。
拒答必须成为工具的一种正常结果,不能让模型把它误认成等待补全的空白。
下面这段工具代码把业务拒答转换为 DECLINED:,成功时则把答案和证据一起返回:
import json, os, time
import requests
from langchain.tools import tool
RETRYABLE = {429, 500, 503}
@tool
def ask_factql(question: str) -> str:
"""Answer a question about the company's data (sales, customers, orders...).
Returns an answer with the query and rows behind it, or DECLINED if the
data can't support an answer."""
for attempt in range(3):
resp = requests.post(
"https://api.factql.ai/query",
headers={"X-API-Key": os.environ["FACTQL_API_KEY"]},
json={"q": question},
timeout=120,
)
if resp.status_code in RETRYABLE and attempt < 2:
time.sleep(int(resp.headers.get("Retry-After", 2 ** (attempt + 1))))
continue
resp.raise_for_status()
break
answer = resp.json()
if answer.get("result") != "success":
return "DECLINED: " + answer.get("explanation", "FactQL could not verify an answer.")
data = answer.get("data") or {}
return json.dumps({
"answer": answer.get("explanation"),
"query": answer.get("sql"),
"columns": data.get("columns"),
"rows": (data.get("rows") or [])[:10],
"lineage": (answer.get("lineage") or {}).get("text"),
})
最该检查的是 answer.get("result") != "success" 这一行,它负责识别 HTTP 成功背后的业务拒答。重试则只针对 429、500、503,最多尝试 3 次,优先采用 Retry-After,否则按公式等待 2、4 秒。重复提交一个已经明确拒答的问题,通常不会补出缺失的数据。
给模型证据,还要明确禁止它补数字
成功路径也有设计重点:工具没有只返回一句答案,而是同时返回 SQL、列名、最多 10 行结果,以及 lineage。这里的 lineage 可以理解为数据来源和处理过程的说明,方便追溯一个数字从哪里来。
这些信息既能帮助 agent 解释答案,也能进入日志,供开发者排查。工具的 docstring 同样参与运行:模型会读取它来判断什么时候调用工具,提前说明可能返回 DECLINED,能让拒答进入正常交互流程。
但工具表达清楚,还需要模型遵守。作者的系统提示如下:
SYSTEM = (
"You answer questions about the user's business data using the ask_factql tool. "
"Only state numbers that come back from the tool. If the tool returns DECLINED, "
"tell the user you can't verify that from their data and pass on the explanation. "
"Never estimate or fill in a number yourself."
)
要留意最后一句:不允许估算,也不允许自行补数字。作者认为,缺少这一句,模型容易把拒答改写成「大概是……」。不过原文没有提供不同提示词之间的对照数据,这属于作者的经验判断。
接入使用 langchain.agents.create_agent,传入 tools=[ask_factql] 和 system_prompt=SYSTEM。模型读取环境变量 AGENT_MODEL,默认值是 openai:gpt-5.4-mini,通过 agent.invoke 传入用户消息。
安装命令是 pip install "langchain>=1" langchain-openai requests,并配置 FACTQL_API_KEY、OPENAI_API_KEY。数据需要先上传 CSV,或在 FactQL 中连接 Postgres、MySQL。
仍使用 LangChain 0.x 的项目,原文给出的入口是 langgraph.prebuilt.create_react_agent(model, [ask_factql], prompt=SYSTEM);v1 改用 create_agent,参数也从 prompt 改为 system_prompt。
拒答测试到底证明了什么
作者建议先问数据能够覆盖的问题,再问明确超出范围的问题。例如,只有销售数据时,分别执行:
python agent.py "Which region had the highest revenue last month?"
python agent.py "What was our ad spend last quarter?"
第二个问题应该得到无法核实的说明,而不是广告支出数字。这个测试比只检查「能否查出营收」更接近文章要解决的风险。
原文还提供了不消耗模型 token 的离线测试:用 GenericFakeChatModel 的子类 ToolFake 模拟工具调用,用 mock.patch("requests.post", return_value=declined) 返回 HTTP 200、result: error 的响应,最后检查工具消息是否满足 startswith("DECLINED:")。
这里需要分清验证边界。假模型的最终回复已经预先写好,因此测试证明的是拒答标记能够通过调用链传回来,并没有证明真实模型一定遵守提示词。把它放进 CI,可以防止重构悄悄破坏工具返回约定;真实模型的行为仍需另外检查。
部署方面,作者要求密钥留在服务端,不能发送到浏览器。原文列出的免费计划限制是 /query 每个 key 每分钟 30 次请求、每月 $10 花费上限,到达上限返回 402。这属于额度错误,需要与数据无法回答分开处理。
我的判断
这套做法最有价值的地方,是把「没有证据」变成可识别、可记录、可测试的状态。相比工具只返回自然语言答案,它更容易定位问题,也更适合做业务数据问答。
但带证据不等于证据已经正确,提示词也不等于强制校验。 SQL 是否符合业务口径、时间范围是否准确、来源是否可信,仍要检查。返回前 10 行有助于追溯,也不能单独证明整个汇总结论正确。
代码还有一个边界:只要 result 是 success,即使 data 为空,仍然会返回成功形态。它依赖 FactQL 在服务端准确判断哪些结果值得拒答。换成自建工具时,这部分规则需要自己定义。
作者明确披露自己参与 FactQL 开发;原文没有给出错误率、测试集或替代方案的对照结果。我认可这个工程模式,但不会据此判断 FactQL 比其他数据工具更可靠。对于严格的数据展示场景,还可以让程序直接渲染经过校验的数值,把模型限制在解释文字上。
能拿来做什么
- 给现有业务问答工具补上成功、无法核实和调用失败三类结果,成功时附带查询与数据来源;前端据此展示答案或拒答解释,避免把空结果继续交给模型补写。
- 在 CI 中保留 HTTP 200 但业务拒答的测试,再用真实模型检查超出数据范围的问题。前者保护调用链,后者检查模型是否遵守约定。
- 按周整理拒答日志,判断用户的问题究竟需要补表、补字段,还是需要更清楚的提问引导,别把每次拒答都当成模型能力不足。
- 如果 agent 或编辑器支持 MCP,可以评估 FactQL 的托管 MCP 接口
/mcp或开源项目factql/factql-mcp,其ask工具采用相同的回答或拒答形态。需要 Next.js route、嵌入式聊天或 OpenAI-style function calling 示例时,原文指向 MIT 许可的factql/factql-examples。