工具返回与中间层
Q:如果工具调用是成功的,但返回结果语义不完整,模型很容易误判,你怎么设计中间层?
来源:腾讯大模型应用开发二面
新手答:“让模型自己判断返回值是否完整。”
高手答:
这个问题非常常见。很多工具从接口层面看是 200 成功,但业务语义上其实不够用——比如只返回了一个 code 没有返回解释信息,或者字段含义不清,模型会自行脑补。
解决方式是加一个 tool adapter 或 semantic wrapper,把原始结果转成统一、可解释的中间表示:
- 字段补全:缺失字段用默认值或“未知”显式标注,而不是让模型猜
- 错误翻译:把错误码转成自然语言描述,比如
code: 404→"未找到对应记录" - 单位归一:统一数据格式,比如日期统一成 ISO 格式、金额统一带币种
- 空值处理和置信度标注:区分“查了没有”和“没查到”,标注数据可信度
核心原则:不要把外部 API 的脏数据直接回喂给模型。中间层先清洗,让模型看到的是“可推理对象”,而不是原始接口垃圾。
差距在哪:新手让模型自己判断——模型没有业务语义知识,判断不了。高手在工具和模型之间加了一个 semantic wrapper 层,做字段补全、错误翻译、单位归一和置信度标注。面试官考的是你有没有意识到“工具调用成功 ≠ 结果可用”,以及怎么在中间层做数据治理。
追问:外部工具返回的数据格式和 Agent 期望的不匹配,怎么做自动映射?
来源:淘天 AI Agent 二面
这在接入第三方 API 时非常常见——天气 API 返回 temp_f(华氏度),Agent 期望 temperature_celsius(摄氏度);电商 API 返回嵌套 JSON,Agent 期望扁平 key-value。
三层映射方案:
| 层次 | 处理内容 | 实现方式 |
|---|---|---|
| 结构映射 | 字段重命名、嵌套展平、数组提取 | JSONPath / JMESPath 表达式 |
| 类型转换 | 单位换算、日期格式、枚举值映射 | 预定义转换函数库 |
| 语义补全 | 补充缺失字段、添加业务含义注释 | 模板填充 + 默认值规则 |
追问:MCP 接入多个测评工具,同一问题返回格式不统一,怎么处理?
来源:阿里淘天 AI应用开发一面
MCP 接多工具后,同一个“代码质量评测”请求,SonarQube 返回 JSON、ESLint 返回文本列表、Pylint 返回 CSV 格式——格式不统一模型根本无法做交叉对比。
解决方案是在 MCP Server 内部做标准化输出层:
| 层级 | 做什么 | 示例 |
|---|---|---|
| 格式归一 | 所有工具输出统一为同一 JSON Schema | \{tool, severity, message, location\} |
| 语义对齐 | 不同工具的等级/标签映射到统一枚举 | SonarQube “BLOCKER” = ESLint “error” = “严重” |
| 结果聚合 | 多工具结果合并去重后再返回 Agent | 相同位置的相同问题只保留一条 |
核心原则:格式统一是 MCP Server 的职责,不是模型的职责——让模型去理解三种不同格式是浪费 token 且不可靠。
工程实现——Schema 适配器模式:
工具注册时定义两份 Schema:
raw_schema:工具实际返回的原始格式
agent_schema:Agent 期望的标准格式
映射规则(声明式配置,不写代码):
{
“temperature_celsius”: “$.main.temp_f | fahrenheit_to_celsius”,
“city_name”: “$.name”,
“wind_speed_mps”: “$.wind.speed | mph_to_mps”,
“description”: “$.weather[0].description”
}声明式配置的好处是新增工具不需要写代码——只需要写一份映射规则。转换函数(如 fahrenheit_to_celsius)是预置的可复用单元。
当映射规则覆盖不了的复杂情况(如返回格式不固定),才降级到用 LLM 做动态格式转换——但这会引入延迟和不确定性,只作为兜底方案。
Q:工具多导致 token 数过多,怎么解决?
来源:字节 Agent 实习二面 【阿里国际一面追问:Skill描述过长导致上下文爆炸】【小红书 Agent 岗一面追问:工具延迟加载的触发时机与具体实现】
新手答:“减少工具数量。”
高手答:
砍工具是下策——工具是 Agent 的能力,砍多了能力就残了。核心思路是让模型在每次调用时只看到需要的工具,而不是全量工具。
1. 动态工具加载:
- 不把所有工具的 schema 一次性塞进 system prompt
- 根据用户当前意图,只加载相关工具。比如用户在问天气,就只加载天气/地理类工具,不加载数据库/文件操作类工具
- 意图识别可以用轻量分类器或关键词规则,成本极低
2. 工具描述压缩:
- 很多工具描述写得太冗长——把 200 字的描述压缩到 50 字以内,只保留“这个工具做什么”和“关键参数”
- 参数的详细约束放在二级描述中,模型选中工具后再加载完整 schema
3. 分层工具组织:
第一层:工具大类(5-8 个)
→ 信息查询类 / 数据操作类 / 文件处理类 / 通信类 / ...
第二层:具体工具(每类 5-10 个)
→ 天气查询 / 股票查询 / 地图搜索 / ...模型先选大类,再在大类下选具体工具。两次选择的 token 开销远小于一次展示全量工具。
4. 工具向量检索(已有工具百级以上时):
- 用户 query 做 embedding,和工具描述做语义匹配,只召回 top 3-5 的工具
- 这就是把“工具选择”变成了一个“检索”问题
什么时候触发延迟加载:
延迟加载不是等模型已经生成了一个不存在的工具名再补救。更稳的触发点有两级:
- 模型调用前:意图路由器根据当前用户请求、任务阶段和已有 plan 召回候选工具,只把候选工具的摘要或 schema 注入本轮请求。
- 模型选中能力后:若第一层只披露了工具类别或 Skill 摘要,编排器再加载完整说明、参数 schema、权限和示例,随后重新发起受约束的决策轮。
工具注册表保存名称、版本、能力标签、embedding、schema URI、权限和健康状态;路由结果还要经过租户权限与可用性过滤。缓存可以减少重复加载,但缓存键必须包含工具版本和权限作用域,避免把旧 schema 或其他用户可见的工具带入当前会话。
差距在哪:新手只会说“动态加载”,但没有交代加载发生在哪一轮、谁负责路由、完整 schema 从哪里取以及权限如何过滤。高手能把渐进式披露落实为注册表、两级触发、版本化缓存和受控重决策。
Q:Mock 是怎么实现的?在自动化生成测试的场景下
来源:抖音基础架构 Agent 一面
新手答:“用 unittest.mock 替换依赖。”
高手答:
Mock 的实现分三步:识别依赖 → 生成 Mock 对象 → 注入测试。
第一步:识别需要 Mock 的依赖
从 AST 和 import 分析中提取外部调用——数据库连接、HTTP 请求、文件 I/O、第三方 SDK 调用。判断标准是:这个依赖在测试环境里能不能正常运行。能运行的不 Mock,不能运行的才 Mock。
第二步:生成 Mock 对象
根据依赖的接口签名生成 Mock:
- 函数调用:Mock 返回值,类型和实际返回一致
- 类实例:Mock 整个类,保留方法签名,返回值用合理的默认值
- 异步调用:生成
AsyncMock,保持await语义
# 自动生成的 Mock 示例
@patch('module.db_client.query')
def test_get_user(mock_query):
mock_query.return_value = {"id": 1, "name": "test"}
result = get_user(1)
assert result["name"] == "test"
mock_query.assert_called_once_with(user_id=1)第三步:注入策略
不同语言注入方式不同——Python 用 @patch 装饰器或 with 语句,Java 用依赖注入框架(Mockito),JavaScript 用 jest.mock。关键是 Mock 的粒度:Mock 太多会让测试脱离真实行为,Mock 太少会因为环境依赖跑不起来。
差距在哪:新手只知道用 Mock 库替换依赖。高手的回答展示了自动化场景下的完整 Mock 生成流程——先识别、再生成、再注入——且点出了 Mock 粒度的核心取舍。面试官考的不是你会不会用 Mock 库,而是你能不能让 Agent 自动决定“Mock 什么、怎么 Mock”。