基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-09 21:01

工具调用成功,也可能查错了对象

dev.to · AI#参数校验#工具调用#Agent
Editor brief · 编辑速览

薪资查询工具在 roles 为空时会回退到默认会计岗位,导致请求通过校验却返回错误对象的数据。文章用合成示例说明,应检查岗位非空、拒绝纯空白输入,并核对返回岗位是否符合用户请求。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

薪资工具返回了结果,Agent 也顺利写出了答案,但回答的可能是另一个岗位的薪资。原文指出,在 salaryBenchmarks 模式下,空的 roles 会选中默认会计岗位。整个调用没有报错,错误反而更容易被包装成可信结论。

作者给出的办法,是在模型和工具之间加一层适配器:查询前检查岗位与市场,查询后保留原始问题和返回岗位,再决定能不能回答。它值得读的地方,是把「工具执行成功」和「用户问题得到回答」拆成了两个必须分别验证的条件。

为什么一次成功调用,仍然可能答错问题

原文中的 Actor 有 41 个输入属性和 5 个 mode 值,但只有 mode 是必填项。 因此,一次薪资请求即使没有说明查询哪个岗位,也可能通过输入 schema 校验。

Actor 可以理解为被调用的采集或查询程序。它顺利执行,只能证明程序完成了一次查询,不能证明查询对象符合用户意图。空岗位触发默认会计岗位后,返回数据看起来依然合理,Agent 很容易直接拿来组织答案。

作者建议把薪资助手的职责收窄:提取用户明确提供的岗位和市场,调用工具,再检查返回岗位。用户没给岗位,就补问;市场也必须明确,因为正确岗位配上错误市场,仍然会得到错误的薪资比较。

这里还有第二层问题:非空字符串也可能提取错,返回岗位也可能比用户要求更宽泛。反过来,标题不同又未必代表错配,缩写和标准化名称都可能造成差异。

所以,输入合法性检查只能挡住一类错误。岗位是否对应,还要在结果回来后继续判断。

把哪些检查写进代码,才能挡住默认值

仅把 roles 设为必填,挡不住空字符串和只包含换行、空白的字符串。作者的适配器按行去除两端空白、过滤空行,再检查是否还剩有效查询,兼容 Actor 原有的多行输入方式。

下面这段代码解决的是「没有有效查询对象,就不允许发起检索」:

def prepare_salary_input(roles=None, country=None):
    if not isinstance(roles, str):
        raise ValueError(
            "Provide roles as text, with a role title or query per line."
        )

    queries = [line.strip() for line in roles.splitlines() if line.strip()]
    if not queries:
        raise ValueError(
            "roles is empty. Ask the user which role to benchmark."
        )

    if country not in ("my", "sg", "id", "ph", "th", "hk"):
        raise ValueError("Provide an explicit supported country code.")

    return {
        "mode": "salaryBenchmarks",
        "roles": "\n".join(queries),
        "country": country,
        "includeRegionalSalaries": False,
        "includeRoleDetails": False,
        "maxItems": 10,
    }

关键是构造 queries 的那一行:校验对象是清理后的有效内容。整个函数还应该在读取凭据、尝试调用 Actor 之前执行。

模型通过 MCP(Model Context Protocol)看到的工具叫 salary_benchmark,只暴露 roles 和 country。其 inputSchema 将二者列入 required;roles 使用 minLength: 1 和 pattern: "\\S",country 使用上述六个代码的 enum,并设置 additionalProperties: false。

schema 负责把契约展示给调用方,Python 函数负责实际执行约束。即使调用方绕过 schema 校验,检查仍然存在。但前提是模型只能经过适配器访问工具;若同时开放原始 Actor,模型仍有绕过检查的路径。

固定的检索选项也留在适配器内,包括关闭区域薪资、关闭岗位详情,以及设置 maxItems: 10。用户问题决定岗位和市场,应用代码决定检索策略。

参数错误应该触发补问,不能触发一次自行编造岗位的重试。 临时连接故障可以考虑重试,缺少查询对象则需要用户补充信息。

返回数据为什么还要带着原始问题

网络调用使用 apify_client 的 ApifyClient,从服务器环境读取 APIFY_TOKEN,通过 client.actor(...).call(run_input=run_input) 调用 vexaloom-labs/jobstreet-southeast-asia-job-search-by-keyword。

作者要求先检查 run["status"] == "SUCCEEDED",否则抛出错误;成功后才通过 defaultDatasetId 和 list_items().items 读取记录。但这仍然只是执行状态检查。

下一步更关键:让原始查询和结果一起进入模型上下文。下面的代码保留岗位身份、金额、单位和来源信息,并限制返回内容的大小:

import json

def salary_context(run_input, items, character_budget=12000):
    fields = (
        "recordType", "roleId", "title", "country",
        "salaryMin", "salaryMax", "salaryMedian",
        "salaryCurrency", "salaryPeriod", "url", "scrapedAt",
    )
    records = []
    for item in items:
        if item.get("recordType") != "salaryBenchmark":
            raise ValueError("Expected salaryBenchmark records.")
        records.append({key: item[key] for key in fields if key in item})

    envelope = {
        "requestedRoles": run_input["roles"].splitlines(),
        "requestedCountry": run_input["country"],
        "records": records,
    }
    serialized = json.dumps(envelope, ensure_ascii=False, separators=(",", ":"))
    if len(serialized) > character_budget:
        raise ValueError(
            "Salary context exceeds the adapter budget. Narrow the request."
        )
    return serialized

def salary_benchmark(roles=None, country=None, *, fetch=fetch_salary_records):
    run_input = prepare_salary_input(roles, country)
    items = fetch(run_input)
    return salary_context(run_input, items)

要留意 requestedRoles 和 records 分别保存的方式。多行查询按批次保留,不能按数组位置逐项配对,因为接口没有承诺返回顺序与查询顺序一一对应。

character_budget=12000 是字符预算,不是 token 上限。实际预算需要给对话和答案留空间;需要精确控制 token 时,应使用对应模型的 tokenizer。

超限时直接拒绝,也有明确理由:截断序列化文本,可能把金额和币种、周期、岗位拆开。作者宁可要求缩小请求,也不接受残缺上下文。

同样,缺少 salaryMedian 就保留缺失,不能补成零。只有岗位名称、没有金额的记录,也不足以支持数值回答。maxItems: 10 还限制了结果数量,因此收到记录不等于覆盖了全部请求岗位。

我的判断

我认同这套方案的优先级:先用代码守住可判定的输入边界,再让模型处理岗位语义。相比单纯在提示词里要求「必须提供岗位」,可执行校验更容易检查,也更容易通过测试确认是否生效。

但它证明的范围需要收紧。原文明确说,适配器示例和记录都是合成的,用来演示校验边界,不验证薪资准确性。作者提供的 fake_fetch 检查了 None、""、" \n\t\n " 都不会触发检索,也检查了多行输入会被规范化。

这些测试没有证明真实工具的岗位匹配质量、薪资可靠性或多岗位覆盖率。原文也没给出提示词方案与适配器方案的错误率对照,因此不能据此宣称整体准确率提升了多少。

此外,这套代码没有完成自动岗位匹配、覆盖率检查,也没有保证薪资字段齐全。它选择保留证据,让后续回答能表达不确定性。对于需要严格薪资对比的产品,这些仍是待补的检查。

MCP 注册 JSON 也只是描述数据,需要服务器真正注册并分发到 salary_benchmark。把描述贴出来,并不等于已经启动了一个可用服务。

能拿来做什么

  • 给现有 AI 工具调用加一层小型适配器,先明确哪些字段构成用户的问题,再检查去空白后的内容。涉及岗位和市场的查询,缺一项就补问。
  • 在 Node 或 Python 的工具服务中,把参数错误与网络错误分开处理。参数错误返回缺失信息,避免自动重试时由模型偷偷补上查询对象。
  • 给检索结果增加原始请求字段,并保留岗位身份、币种和薪资周期。多项查询没有对应关系保证时,不要直接按数组下标拼成比较表。
  • 用可注入的假检索函数测试调用边界,确认无效输入不会触网。接入真实 Actor 前,再单独验证返回岗位、字段完整性和请求覆盖情况。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
用GitHub发布包上架MCP注册表
下一篇
Claude Fable 5.1 的性能与缓存成本分析

资讯速览

dev.to · AI · 2026-11-09 21:01

AI 评分 8.0 / 10

相关标签

#参数校验#工具调用#Agent

继续阅读

  • 用GitHub发布包上架MCP注册表
  • Claude Fable 5.1 的性能与缓存成本分析
  • 接手旧代码库,先核验 AI 的每项结论
  • 用持久化与人工审批补齐 Agent 执行链