☰
工具调用 3|工具拿到参数以后,程序还必须检查什么?
2026/10/14 13:49:07 网站建设 项目流程

假设你问个人资料助手:

帮我查一下保存的读书会通知,报名截止日期是哪天?

要回答这个问题,助手先要找到相应的通知。我们给它提供了一个名叫search_notices的工具,意思是“搜索通知”,对应程序里实际搜索已保存通知的函数。

程序把用户问题交给模型时,也会提供工具说明:这个工具能搜索已保存通知,调用时需要搜索关键词,还可以限定通知的发布日期。模型根据用户问题和这些说明,判断是否请求使用它。这里要核对的是通知中的报名日期,因此搜索通知是合适的操作。

模型请求使用这个工具时,需要把搜索条件交给程序。例如,下面是一份参数示例:

{"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当前用户 Alice2026-09-10报名原定于 9 月 20 日截止
D2当前用户 Alice2026-09-16报名截止日期提前到 9 月 18 日
D3另一位用户 Bob2026-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,运行:

python-X utf8-B tool_guard.py
完整只读练习代码
importjsonfromdataclassesimportdataclassfromdatetimeimportdate@dataclass(frozen=True)classContext:user_id:strcan_search:boolclassRejectedCall(ValueError):passNOTICES=({"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":"九月读书会志愿者报名联系名单,由另一位用户保存。",},)defunique_fields(pairs):result={}forkey,valueinpairs:ifkeyinresult:raiseRejectedCall("invalid_arguments")result[key]=valuereturnresultdefreject_constant(value):raiseRejectedCall("invalid_arguments")defvalidate_arguments(text):ifnotisinstance(text,str)orlen(text)>4096:raiseRejectedCall("invalid_arguments")try:args=json.loads(text,object_pairs_hook=unique_fields,parse_constant=reject_constant,)except(ValueError,RecursionError)aserror:raiseRejectedCall("invalid_arguments")fromerrorifnotisinstance(args,dict)orset(args)!={"query","published_from"}:raiseRejectedCall("invalid_arguments")query=args["query"]ifnotisinstance(query,str)ornot1<=len(query.strip())<=100:raiseRejectedCall("invalid_arguments")since=args["published_from"]ifsinceisnotNone:ifnotisinstance(since,str):raiseRejectedCall("invalid_arguments")try:parsed=date.fromisoformat(since)exceptValueErroraserror:raiseRejectedCall("invalid_arguments")fromerrorifparsed.isoformat()!=since:raiseRejectedCall("invalid_arguments")since=parsedreturnquery.strip(),sincedefuser_notices(user_id):return[rowforrowinNOTICESifrow["owner_id"]==user_id]defsearch_notices(query,since,context):ifnotcontext.can_search:raiseRejectedCall("forbidden")matches=[]forrowinuser_notices(context.user_id):ifsinceisnotNoneanddate.fromisoformat(row["published_on"])<since:continueifall(wordinrow["content"]forwordinquery.split()):matches.append({key:row[key]forkeyin("id","published_on","content")})return{"status":"ok","matches":matches}defexecute_call(name,arguments_text,context):ifname!="search_notices":raiseRejectedCall("unknown_tool")query,since=validate_arguments(arguments_text)returnsearch_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)exceptRejectedCallaserror: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 日?

假如模型把这个日期填成通知发布日期下限:

{"query":"读书会 报名","published_from":"2026-09-18"}

字段齐全,类型正确,日期合法,当前用户也允许搜索。这个请求会通过检查,但在教学资料里实际返回空列表:D1、D2 都是在 9 月 18 日之前发布的。

错在两个日期的关系。用户给的是待核对的报名截止日期,工具参数却被解释成“只查这天及之后发布的通知”。仅检查参数对象,程序不知道这个日期是怎样从问题中取得的,因此无法证明它符合用户原意。

能够确定的业务条件,应继续放进程序检查。例如用户在页面上明确选择了“从 9 月 15 日发布的通知开始查”,程序可以直接带入这个值,或者核对模型参数是否与选择一致。只有自然语言问题、没有独立确定条件时,还需要通过清楚的工具说明、针对性任务和结果核对,检查模型是否理解了日期的用途。不能看到参数校验通过,就把“这次请求允许执行”扩大为“这次搜索一定能回答问题”。

再试着判断两个变化:

  1. 搜索工具定义发给模型时,用户有权限;模型返回参数之前,权限已经被撤销。仍然执行吗?
  2. 模型请求read_notice("D3"),编号真实存在、格式正确,但 D3 属于 Bob。Alice 可以读取吗?

第一种情况要以实际执行时的权限为准。第二种情况,即使程序提供了按编号读取的工具,也要在读取那份通知时检查归属或授权;编号存在不等于当前用户可以访问。搜索入口有范围控制,不能代替另一个读取入口的检查。

回到开头,程序拿到参数后,需要确认操作名称、参数结构、可确定的取值条件、当前权限与实际访问范围。通过这些检查,才允许在限定范围内执行;执行是否成功、结果是否足以支持回答,仍要依据真实返回继续判断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询