工具调用成功,也可能查错了对象
薪资查询工具在 roles 为空时会回退到默认会计岗位,导致请求通过校验却返回错误对象的数据。文章用合成示例说明,应检查岗位非空、拒绝纯空白输入,并核对返回岗位是否符合用户请求。
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 前,再单独验证返回岗位、字段完整性和请求覆盖情况。