假设你问个人资料助手:
帮我查一下保存的读书会通知,报名截止日期是哪天?
要回答这个问题,助手先要找到相应的通知。我们给它提供了一个名叫 search_notices 的工具,意思是"搜索通知",对应程序里实际搜索已保存通知的函数。
程序把用户问题交给模型时,也会提供工具说明:这个工具能搜索已保存通知,调用时需要搜索关键词,还可以限定通知的发布日期。模型根据用户问题和这些说明,判断是否请求使用它。这里要核对的是通知中的报名日期,因此搜索通知是合适的操作。
模型请求使用这个工具时,需要把搜索条件交给程序。例如,下面是一份参数示例:
json
{
"query": "读书会 报名",
"published_from": null
}
这里,query 是搜索关键词;published_from 用来限定通知的发布日期,null 表示这次不加日期限制。
接下来,程序要把这份请求变成真正的搜索。执行前需要确认:工具名是否在允许范围内,参数能否被搜索函数正确处理,以及本次搜索应读取哪些资料。下面就沿这次查询,逐项看这些检查解决什么问题。
1. 参数能解析,还要检查它是否符合这项操作的约定
先明确这个搜索工具接受什么:query 是用空格分隔的关键词;published_from 是通知发布日期的下限,包含当天,使用 YYYY-MM-DD;不筛选发布时间时填 null。
收到调用后,程序至少要分清下面几种情况:
| 收到的内容 | 为什么不能直接搜索 | 程序可以怎样检查 |
|---|---|---|
工具名为 delete_notices |
本次执行入口只提供通知搜索 | 只接受程序预先登记的工具名 |
参数文字只有 {"query":"读书会" |
JSON 尚未完整,无法确定参数对象 | 完整解析,失败就拒绝,不补出缺失部分 |
query 为 123,或缺少 published_from |
字段类型或字段集合不符合约定 | 检查必填字段、额外字段及类型 |
query 全是空格,或长达几千个字符 |
类型虽然是字符串,却不是本例允许的查询 | 去除首尾空白后检查内容与长度 |
published_from 为 "2026-02-30" |
写法像日期,但日历上没有这一天 | 按日期解析,并检查规定的格式 |
前面几项主要检查"交来的是什么",后面几项检查"这个值是否可用"。只要求日期是字符串,拦不住不存在的日期;只确认搜索词是字符串,也拦不住空查询。
长度限制同样需要依据具体实现。在这个小例子里,我们把参数文字限制为 4096 个字符,把去除首尾空白后的关键词限制为 100 个字符,避免把过大的输入交给解析器或搜索函数。这是教学程序的取值,不是所有搜索工具都应采用的标准。实际服务还要根据请求大小、搜索成本等条件确定限制。
模型接口提供的结构约束可以减少格式错误,程序侧仍应执行自己依赖的检查。尤其是当前权限、资料归属等条件,不能由生成参数的结构约束决定。
检查也不应顺带扩大工具能力。程序只登记了搜索,就只调用对应的搜索函数;不能把模型给出的名称当作代码执行,也不能因为名称看起来像某个函数,就在整个程序里寻找它并调用。
2. 访问谁的资料,要由程序在读取时决定
现在给教学资料增加一位用户。这个练习不设置共享授权,每位用户只能读取自己保存的通知。三份通知都含有"读书会"和"报名",但归属不同:
| 通知 | 保存者 | 发布日期 | 主要内容 |
|---|---|---|---|
| D1 | 当前用户 Alice | 2026-09-10 | 报名原定于 9 月 20 日截止 |
| D2 | 当前用户 Alice | 2026-09-16 | 报名截止日期提前到 9 月 18 日 |
| D3 | 另一位用户 Bob | 2026-09-16 | 读书会志愿者报名联系名单 |
Alice 搜索"读书会 报名",关键词能够匹配三份资料。但属于她的结果只能是 D1、D2。匹配关键词和允许读取,是两个不同的条件。
程序需要知道两个值:当前登录用户是谁,以及这个用户当前是否允许搜索。这些值应来自程序可信的登录会话和权限判断,不是模型在参数里填写的身份。模型输出一个 user_id,只能说明它生成了这个字符串,不能证明这次请求属于该用户。
因此,模型可填写的仍然只有 query 和 published_from。执行时,程序另外带入当前用户,并在数据读取处限定范围:

这个顺序很重要。如果先把所有人的通知交给模型,再要求它"只回答 Alice 的资料",其他人的内容已经进入了模型输入。即使最终答案没有显示 D3,也不能说访问范围得到了控制。限定范围必须发生在资料交给模型之前;查询数据库时,应把用户或授权范围作为数据查询条件落实。
公开工具实现中也能看到这种身份来源的划分。LangChain 官方工具文档的账户查询示例中,应用传入 UserContext(user_id=...),账户查询函数从 runtime.context.user_id 取得用户编号,再读取对应账户。文档还说明,runtime 不属于模型填写的工具参数。
这里值得借鉴的是:身份通过程序提供的上下文进入工具,函数按这个身份取数据。这个公开示例没有展示完整的登录验证或权限系统;把身份字段从模型参数中移走以后,应用仍要保证传入的身份可信,并落实实际访问规则。
只读工具也需要这项控制。搜索不会修改通知,但读到另一个人的资料,本身就已经越过了允许访问的范围。
3. 用一个能运行的小程序,观察检查怎样阻止执行
下面的练习只搜索三份虚构通知,不联网、不调用模型。输入是已经从完整工具调用中提取出的工具名和参数文字,便于单独观察执行检查。真实接入时,接收层还应先确认响应和调用内容完整,不能把未完成的参数送进执行入口。
代码中的 Context 保存程序提供的用户与权限。为了能在本地直接运行,示例手工固定这两个值;接入真实应用时,必须由服务端根据登录会话和执行时的权限产生,不能照搬浏览器请求或模型输出中的同名字段。
使用 Python 3.10 或更高版本,无需安装额外依赖。将下面的完整程序命名为 tool_guard.py,运行:
powershell
python -X utf8 -B tool_guard.py
完整只读练习代码
python
import json
from dataclasses import dataclass
from datetime import date
@dataclass(frozen=True)
class Context:
user_id: str
can_search: bool
class RejectedCall(ValueError):
pass
NOTICES = (
{
"id": "D1", "owner_id": "u-alice", "published_on": "2026-09-10",
"content": "本次读书会报名将于2026年9月20日截止。",
},
{
"id": "D2", "owner_id": "u-alice", "published_on": "2026-09-16",
"content": "本次读书会报名截止日期提前到2026年9月18日,原通知中的截止日期作废。",
},
{
"id": "D3", "owner_id": "u-bob", "published_on": "2026-09-16",
"content": "九月读书会志愿者报名联系名单,由另一位用户保存。",
},
)
def unique_fields(pairs):
result = {}
for key, value in pairs:
if key in result:
raise RejectedCall("invalid_arguments")
result[key] = value
return result
def reject_constant(value):
raise RejectedCall("invalid_arguments")
def validate_arguments(text):
if not isinstance(text, str) or len(text) > 4096:
raise RejectedCall("invalid_arguments")
try:
args = json.loads(
text, object_pairs_hook=unique_fields, parse_constant=reject_constant,
)
except (ValueError, RecursionError) as error:
raise RejectedCall("invalid_arguments") from error
if not isinstance(args, dict) or set(args) != {"query", "published_from"}:
raise RejectedCall("invalid_arguments")
query = args["query"]
if not isinstance(query, str) or not 1 <= len(query.strip()) <= 100:
raise RejectedCall("invalid_arguments")
since = args["published_from"]
if since is not None:
if not isinstance(since, str):
raise RejectedCall("invalid_arguments")
try:
parsed = date.fromisoformat(since)
except ValueError as error:
raise RejectedCall("invalid_arguments") from error
if parsed.isoformat() != since:
raise RejectedCall("invalid_arguments")
since = parsed
return query.strip(), since
def user_notices(user_id):
return [row for row in NOTICES if row["owner_id"] == user_id]
def search_notices(query, since, context):
if not context.can_search:
raise RejectedCall("forbidden")
matches = []
for row in user_notices(context.user_id):
if since is not None and date.fromisoformat(row["published_on"]) < since:
continue
if all(word in row["content"] for word in query.split()):
matches.append({key: row[key] for key in ("id", "published_on", "content")})
return {"status": "ok", "matches": matches}
def execute_call(name, arguments_text, context):
if name != "search_notices":
raise RejectedCall("unknown_tool")
query, since = validate_arguments(arguments_text)
return search_notices(query, since, context)
if __name__ == "__main__":
context = Context(user_id="u-alice", can_search=True)
arguments = '{"query":"读书会 报名","published_from":null}'
try:
result = execute_call("search_notices", arguments, context)
except RejectedCall as error:
result = {"status": "rejected", "reason": str(error)}
print(json.dumps(result, ensure_ascii=False, indent=2))
这里最值得跟着看的是三处:execute_call 只接受登记的工具名,validate_arguments 检查并解释参数,search_notices 在取得资料前检查权限,再通过 user_notices 取得当前用户范围内的通知。
两个 JSON 解析辅助函数用来拒绝重复字段以及 NaN 等非标准数值。Python 的 JSON 文档说明,默认解码器会接受这些情况;如果参数约定不允许,就需要明确拒绝。日期使用标准库的 日期解析方法,再确认格式与约定一致。
直接运行默认输入,实际返回 status="ok",通知编号为 D1、D2,没有 D3。再用人工准备的请求分别触发几种条件,得到以下结果。这些输入用于验证程序的检查行为,不代表模型在实测中生成过这些错误。
| 请求变化 | 实际处理结果 | 是否进入资料读取 |
|---|---|---|
| 当前用户 Alice,关键词为"读书会 报名",不筛选发布时间 | 成功,返回 D1、D2 | 是,范围为 Alice 的通知 |
将发布日期下限改为 "2026-09-15" |
成功,只返回 D2 | 是,范围仍为 Alice 的通知 |
将 query 改为数字 123 |
拒绝,invalid_arguments |
否 |
将日期改为 "2026-02-30" |
拒绝,invalid_arguments |
否 |
将工具名改为 delete_notices |
拒绝,unknown_tool |
否 |
在参数里额外加入 "user_id":"u-bob" |
拒绝,invalid_arguments |
否 |
保留正常参数,但程序提供的 can_search 为 False |
拒绝,forbidden |
否 |
这份结果要核对的不只是返回文字。检查程序同时观察了资料读取函数有没有被调用:上表中被拒绝的请求都没有进入该函数。把执行时的用户改为 Bob,正常请求只返回 D3,也说明范围随程序提供的身份变化,没有固定成某一位用户。
示例在内存中按保存者筛选资料,便于直接看见规则。接入真实系统时,需要在服务端的资料服务或数据库访问处落实同样的边界;本地手工指定的身份与权限不能代替登录验证。
4. 拒绝以后,不能为了"让调用成功"悄悄改掉条件
检查发现问题时,处理方式应保留原任务的含义。去除关键词首尾空白,是本例允许的整理;把不存在的 2 月 30 日改成 2 月 28 日,却已经改变了查询条件。
同样,缺少 published_from 不应被自动解释成"从今天开始",空字符串也不应悄悄变成 null。前者可能漏掉需要核对的旧通知,后者掩盖了调用方没有遵守约定的问题。程序没有足够依据时,应拒绝这份参数,让调用方纠正,必要时向用户确认。
权限拒绝则需要另一种处理。当前用户不允许搜索,不能靠换一组关键词恢复资格,也不能改成搜索其他用户。只能在程序确认权限已恢复之后,重新判断新的调用能否执行。
练习代码用 RejectedCall 表示这些拒绝,运行入口把它显示为 status="rejected"。这与"查询成功,但没有匹配通知"不同:前者没有执行查询,后者已经在允许范围内查过。记录和展示时保留这个区别,才不会把权限失败误解成资料不存在。
5. 检查通过,为什么仍然可能查错问题
最后看一个程序不能靠上述规则识别的错误。用户问:
报名截止日期是不是 2026 年 9 月 18 日?
假如模型把这个日期填成通知发布日期下限:
json
{
"query": "读书会 报名",
"published_from": "2026-09-18"
}
字段齐全,类型正确,日期合法,当前用户也允许搜索。这个请求会通过检查,但在教学资料里实际返回空列表:D1、D2 都是在 9 月 18 日之前发布的。
错在两个日期的关系。用户给的是待核对的报名截止日期,工具参数却被解释成"只查这天及之后发布的通知"。仅检查参数对象,程序不知道这个日期是怎样从问题中取得的,因此无法证明它符合用户原意。
能够确定的业务条件,应继续放进程序检查。例如用户在页面上明确选择了"从 9 月 15 日发布的通知开始查",程序可以直接带入这个值,或者核对模型参数是否与选择一致。只有自然语言问题、没有独立确定条件时,还需要通过清楚的工具说明、针对性任务和结果核对,检查模型是否理解了日期的用途。不能看到参数校验通过,就把"这次请求允许执行"扩大为"这次搜索一定能回答问题"。
再试着判断两个变化:
- 搜索工具定义发给模型时,用户有权限;模型返回参数之前,权限已经被撤销。仍然执行吗?
- 模型请求
read_notice("D3"),编号真实存在、格式正确,但 D3 属于 Bob。Alice 可以读取吗?
第一种情况要以实际执行时的权限为准。第二种情况,即使程序提供了按编号读取的工具,也要在读取那份通知时检查归属或授权;编号存在不等于当前用户可以访问。搜索入口有范围控制,不能代替另一个读取入口的检查。
回到开头,程序拿到参数后,需要确认操作名称、参数结构、可确定的取值条件、当前权限与实际访问范围。通过这些检查,才允许在限定范围内执行;执行是否成功、结果是否足以支持回答,仍要依据真实返回继续判断。