面试官可能会问:“Function Calling 是模型直接调用了后端函数吗?从用户提问到最终回答,中间完整链路是什么?”
这道题最常见的错误,是把模型画成一个会访问数据库、会执行 Python、会请求外部 API 的黑盒。实际上,模型通常只能看到工具说明,然后输出一个结构化调用请求。解析参数、鉴权、执行函数、处理异常和回传结果,全部由宿主程序负责。模型下一轮能否基于真实结果回答,还取决于程序是否把工具请求和工具结果按协议加入上下文。
更准确地说,Function Calling 是模型与程序之间的一份结构化协作协议,并没有赋予模型直接执行函数的能力。把协议中的角色和状态讲清,才能从一次天气查询 Demo 继续讲到可控 Agent。
先给出 30 秒回答
Function Calling 的完整链路是:应用把用户消息和工具 Schema 一起发给模型;模型判断是否需要工具,返回包含工具名、参数和调用 ID 的结构化请求;宿主程序解析并校验参数,检查权限,再调用真实函数或服务;程序把执行结果以 tool message 形式回传,并关联原调用 ID;模型在下一轮读取结果,决定继续调用工具还是生成最终答案。模型只负责提出调用意图和使用工具结果,不直接执行函数。生产实现还要处理空调用、未知工具、参数错误、多工具调用、超时、重复副作用、结果截断和循环上限。
这段回答里,职责分工最重要:模型负责选择和生成参数,程序负责验证和执行,工具负责返回事实,模型再负责解释或继续决策。

把模型、宿主程序、工具和用户分开,很多误解会自然消失。
第一步:模型看到的不是函数代码,而是工具说明
模型通常看不到后端函数的实现。你需要把工具转换成一份它能理解的 Schema,至少包含工具名、用途描述和参数约束。
tools = [{
"type": "function",
"function": {
"name": "search_order",
"description": "根据订单号查询当前登录用户可见的订单状态",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号"
}
},
"required": ["order_id"]
}
}
}]
这段 Schema 是模型的“工具说明书”,不是执行代码。后端还需要有真正的 search_order 实现,并在自己的注册表中建立同名映射。
工具名要稳定。描述要说明“做什么、什么时候用、什么时候不要用”。参数要限制类型、必填项、枚举和范围。模型不知道你的隐含业务规则,如果只写“查询信息”,它很难判断应该查订单、查知识库还是搜索网页。
描述也不能承担安全责任。你可以写“只能查询当前用户订单”,但真正执行前仍然必须从服务端身份获得用户 ID,再做数据权限检查。不能相信模型传入的 user_id,也不能因为 Prompt 说“不要越权”就跳过后端鉴权。
第二步:模型返回的是调用请求,不是调用结果
应用把 messages 和 tools 发给模型后,模型有两种主要选择。
如果它认为不需要工具,可能直接返回普通文本。如果它认为需要工具,会在响应中返回一个或多个 tool call。每个调用一般包含调用 ID、工具名和序列化参数。
{
"id": "call_x1",
"type": "function",
"function": {
"name": "search_order",
"arguments": "{\"order_id\":\"SO001\"}"
}
}
看到这段 JSON 时,订单还没有被查询。它只表达“模型建议调用 search_order,并给出参数”。程序要先判断 tool_calls 是否存在,再逐个处理。直接取数组第一项而不判空,是最常见的新手错误之一。
参数经常以 JSON 字符串形式返回,需要安全解析。解析成功也不代表合法:订单号可能为空,字段可能多出,枚举可能越界,模型还可能给出注册表里不存在的工具名。因此执行之前至少要经过名称匹配、Schema 校验和业务校验。
第三步:宿主程序才是真正的执行者
宿主程序可以是 Python 服务、Java 服务、Node 服务或 Agent 运行时。它收到 tool call 后,按工具名从注册表找到真实执行器。
registry = {
"search_order": search_order,
"search_document": search_document,
}
for call in assistant_message.tool_calls:
name = call.function.name
args = json.loads(call.function.arguments)
validate_schema(name, args)
check_permission(current_user, name, args)
result = registry[name](**args)
这段代码仍然是教学骨架,生产实现还需要超时、限流、审计、幂等和错误分类。关键点在于,执行权始终留在程序侧。
如果工具是只读查询,可以在权限通过后执行;如果工具有写入、副作用或费用,就要增加更严格的流程。比如先生成 Dry-run 结果,让用户确认,再由固定 Workflow 提交。模型可以建议动作,不能绕开审批和资源上限。
工具返回值也不应该直接把内部堆栈、数据库对象或完整网页交给模型。程序要把结果转换成稳定、可理解、大小受控的 Observation。错误同样结构化,例如错误类别、是否可重试、用户需要补什么信息。这样模型才有依据决定重试、换工具或停止。
第四步:把工具结果回灌,模型才能“看见”
工具执行完成后,应用要把两类消息追加到对话:模型刚才发出的 assistant tool call,以及与调用 ID 对应的 tool result。
messages.append(assistant_message)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False)
})
为什么两条都要保留?因为下一轮模型需要知道自己发出了哪个请求,也需要知道哪个结果对应哪个调用。只追加结果、不保留调用请求,或者丢失 tool_call_id,可能让协议链条断裂。
然后应用再次调用模型。模型基于工具结果可以生成最终回答,也可以继续请求另一个工具。例如先搜索拿到网页地址,再访问网页获得正文,最后调用计算工具处理数字。这时 Function Calling 已经从单次握手变成一个工具结果循环。
循环的出口不能只依赖模型“自觉结束”。应用还要限制最大步数、总耗时、调用预算,并检测重复动作。模型连续用相同参数调用同一工具,或者多轮没有新增信息时,应停止、换路或转人工。

真正的 Agent 循环发生在应用里:请求模型、执行工具、回传结果、再次请求模型。
一段完整但仍保持最小的伪代码
下面把各阶段串在一起:
def run_agent(user_text, current_user, max_steps):
messages = [{"role": "user", "content": user_text}]
state = build_initial_state(user_text, current_user)
for _ in range(max_steps):
msg = call_model(messages=messages, tools=public_schemas(current_user))
messages.append(msg)
if not msg.tool_calls:
completion = validate_completion(state, msg)
return {
"status": completion.status,
# status 只能是 done / waiting_user / insufficient / failed
"answer": completion.answer,
"reason": completion.reason,
}
for call in msg.tool_calls:
try:
spec, executor = registry.resolve(call.function.name)
args = parse_and_validate(spec, call.function.arguments)
authorize(current_user, spec, args)
result = execute_with_timeout(executor, args)
observation = normalize_success(result)
except Exception as error:
observation = normalize_error(error)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": serialize(observation),
})
state = apply_observation(state, call, observation)
return {"status": "stopped", "reason": "step_limit"}
这里的 not msg.tool_calls 只代表模型没有继续发起工具请求,不代表业务已经完成。validate_completion 还要根据任务目标、必需字段、工具结果和停止原因,明确返回 done、waiting_user、insufficient 或 failed。
这段代码刻意没有写真实订单、客户或生产指标。它用于说明协议边界,实际项目必须结合模型 SDK 的消息格式、业务身份、工具注册表和状态存储实现。
多个工具调用时,串行还是并行
模型可能在同一轮返回多个 tool call。是否并行不能只看“能不能同时发请求”,还要看依赖与副作用。
两个互不依赖的只读查询,例如查询两个公开资料源,可以并行,然后分别按调用 ID 回灌结果。第二个工具依赖第一个输出时必须串行。例如先搜索得到 URL,再访问对应页面;模型需要先读到搜索结果,下一轮才能生成可靠的访问参数。
有写操作时更谨慎。两个动作即使技术上可以并行,也可能存在业务顺序、锁和补偿要求。工具 Schema 只描述单个调用,跨工具事务应由 Workflow 或状态机管理,不能让模型自由决定提交顺序。
程序还要考虑结果顺序。并行完成顺序可能与请求顺序不同,tool_call_id 用于准确关联。不要靠数组位置猜对应关系。
为什么消息顺序错了,模型就可能接不上
一次工具调用不是孤立 JSON,而是对话协议中的连续事件。通常先有用户消息,然后是包含 tool call 的 assistant 消息,再是一个或多个与调用 ID 对应的 tool 消息,最后才是模型下一轮响应。
程序如果只保留 tool result,却丢掉 assistant 的调用请求,模型看见结果时不知道它回答了哪个动作。反过来,如果保留调用请求却没有补齐所有结果,下一轮可能处在未完成协议状态。并行调用时,每个结果都要关联自己的 ID,不能把多个工具结果拼成一段无法区分来源的文字。
工具结果也要保留状态差异。“没有找到数据”是一次成功执行但结果为空,“接口超时”是暂时失败,“无权限”是禁止执行。三者都写成 error,模型就无法选择正确下一步。统一 Observation 可以包含 status、data、error_type、retryable 和 source,但不要把内部实现细节全部暴露。
消息历史会随循环增长。保留完整调用关系不等于把每个大结果原样重复带给模型。大对象可以存到工件系统,消息里保留摘要、引用和关键字段;旧的稳定结果可以做结构化压缩,但当前未完成调用的请求与结果关联不能被破坏。
一次工具成功,不代表业务任务已经完成
Function Calling 有三个不同层次的“成功”。
第一层是协议成功:模型输出了可解析 tool call,程序也正确回传 tool result。第二层是工具成功:真实函数完成了查询或动作。第三层才是业务成功:用户目标得到满足,必要证据完整,副作用符合规则。
例如教学中的搜索工具正常返回一批网页,只能说明搜索执行成功,不代表已经找到可信答案。教学订单查询接口正常返回“无此订单”,也不代表可以直接告诉用户订单不存在,还可能是身份、环境或订单号有问题。Agent 要根据业务完成条件继续判断,而不能把 HTTP 成功当作任务完成。
这也是为什么最终答案节点要检查 State。它至少要知道用户目标、已调用工具、关键结果、仍缺少的信息和停止原因。模型说“已经完成”只能作为候选信号,程序应验证必要步骤是否真的发生。
怎样设计错误 Observation
工具失败后直接把异常字符串塞给模型,看似省事,实际会造成三类问题:模型看不懂内部堆栈;敏感路径或服务信息被暴露;不同工具错误格式不一致,后续策略无法复用。
可以把错误规范为有限类别,例如参数无效、权限拒绝、资源不存在、超时、上游限流、临时不可用和未知内部错误。再标明是否允许重试、是否需要用户补充、是否存在备用工具。
参数无效通常回到澄清或重新填参;权限拒绝不应通过换个措辞重试;超时可能在预算内重试或降级;资源不存在要核对标识,不能让模型编造结果;未知内部错误对模型只返回安全摘要,详细信息进入日志。
是否可重试由执行层和业务策略决定,不让模型只凭错误文字判断。即使标记可重试,也要受次数、退避和总预算约束。写操作结果不确定时尤其不能自动重试,要先查幂等状态。
用户身份从哪里进入调用链
工具参数分成模型可以生成和系统必须注入两类。查询词、日期范围等业务输入可以由模型从对话提取;当前用户 ID、租户 ID、服务凭证和审计主体必须来自可信运行上下文。
如果 Schema 要求模型填写 user_id,攻击者可以诱导它填入别人的标识。更安全的做法是让公开 Schema 只包含用户可表达的参数,网关在执行时注入身份,再按业务对象授权。日志记录的调用者也来自鉴权上下文,而不是模型输出。
对于代理用户执行的场景,还要区分谁发起任务、哪个服务账号调用下游、最终代表谁产生副作用。三种身份不能只写成一个字符串,否则出现越权时无法追责。
Function Calling 和普通文本解析有什么区别
ReAct 教学 Demo 常让模型输出:
Action: search("关键词")
然后程序用正则解析。这种方式直观,也能兼容没有原生工具协议的模型,但格式容易漂移。模型多写一段解释、引号变化或参数包含特殊字符,都可能让解析失败。
Function Calling 让模型按结构化协议返回工具名和参数,程序更容易解析、校验和关联结果。如果模型和 SDK 支持,生产场景通常优先使用结构化协议。但结构化不等于永远正确:参数仍可能语义错误,工具仍可能选错,Schema 仍可能设计含糊,后端安全校验仍然不能省。
两者的本质都是“模型提出动作,程序执行动作”。区别主要在动作表达是否结构化、解析是否稳定,而不是谁获得了真实执行权。

工具调用不是只有成功和失败,缺参数、越权、暂时故障和业务拒绝需要不同出口。
最容易被忽略的八类失败
模型没有发起工具调用
用户问题需要实时数据,但模型直接凭参数知识回答。可以通过更清楚的工具描述、系统约束和测试样本改进,也可以在业务层识别必须查询的任务并拒绝无证据回答。
模型不该调却调了
简单解释题也发起外部查询,增加成本和风险。需要准备“不调用工具”的负例,并在评估中单独统计无意义调用。
工具名不存在
模型生成了注册表中没有的名字。程序应返回可理解的错误或安全停止,不能通过反射执行任意名称。
参数格式正确但语义错误
JSON 通过校验,不代表业务参数合理。例如日期范围颠倒、资源不属于当前用户。Schema 校验之后仍需要业务校验和权限判断。
工具超时或返回格式变化
工具层要设置超时,错误转成稳定 Observation。可重试错误可以按策略重试,不可重试错误应换路、询问用户或降级。
结果过大
把完整 HTML 或几万行日志回灌,会迅速撑大上下文并干扰模型。工具应在返回前做相关性提取、分页、截断或保存为工件引用。
重复副作用
模型重试写工具可能重复发消息、创建记录或提交动作。写操作必须有幂等键、确认和执行台账。达到不确定状态时不要盲目重试。
模型忽略工具结果
工具返回“未找到”,模型却继续给出确定答案。评估时不能只检查是否调用工具,还要检查最终答案是否忠实使用 Observation,并在证据不足时表达不确定。
怎样评估 Function Calling 做得好不好
只看最终回答是否通顺,会掩盖大量协议错误。可以按链路拆成几层指标,但所有数字都应来自真实评估集,不预先写结论。
第一层是调用决策:该调用时是否调用,不该调用时是否克制。正样本和难负例都要有。
第二层是工具选择:在多个语义相近工具中是否选择正确。测试集要覆盖描述容易混淆的工具,而不是只测每个工具一个明显关键词。
第三层是参数质量:结构是否合法、必填信息是否来自用户上下文、业务值是否合理。格式正确与语义正确要分开统计。
第四层是执行与恢复:超时、空结果、未知工具、权限失败发生时,系统是否走到预期出口,有没有无限重试或绕过权限。
第五层是结果忠实度:最终答案中的事实是否来自工具结果,是否加入了工具没有提供的数字,证据不足时是否明确说明。
第六层是资源与副作用:单任务调用次数、上下文增长、高风险工具审批和重复写入是否受控。
评估样本最好保存完整 Trace:用户输入、可见工具集合、模型 tool call、校验结果、真实工具结果、下一轮模型输出和最终状态。这样一次回归失败才能定位到描述、模型、执行器还是答案整合。

能调通一次只是起点,能处理错误、权限和重复副作用才接近工程系统。
面试官会怎样连续追问
追问一:Function Calling 是不是微调出来的
模型需要具备理解工具 Schema 和生成结构化调用的能力,这可以来自预训练、指令训练或专门数据。应用侧仍要提供当前可用工具说明,并执行所有校验与调用。不要把模型能力和应用协议混成一件事。
追问二:怎么避免模型乱传参数
使用清楚的 Schema 限制类型、枚举、长度和范围;执行前再次解析与校验;缺失信息就向用户澄清;业务身份和权限从服务端上下文获得,不信任模型填写。
追问三:工具报错后要不要把异常直接给模型
不要暴露内部堆栈和敏感信息。把异常映射成稳定、可理解的错误结构,例如类型、是否可重试和建议动作。详细堆栈进入受控日志。
追问四:为什么需要 tool_call_id
它把模型提出的调用与回传结果准确关联,尤其在一轮多个调用、并行执行或多轮历史中,不能依赖顺序猜测。
追问五:模型返回多个工具能否全部执行
先判断依赖、权限和副作用。互不依赖的只读工具可以并行;有依赖的要分轮;写操作要受固定流程、幂等和审批控制,不能因为模型一次返回多个就全部执行。
追问六:Function Calling 为什么还不等于完整 Agent
一次工具握手只证明模型能提出调用。完整 Agent 还需要多步循环、状态、停止条件、失败恢复、权限、Trace、评估和成本控制。
从一次 Tool Call 讲到完整 Agent Loop
Function Calling 提供的是一套结构化握手协议,模型不会直接执行函数。应用先把用户消息和工具 Schema 发给模型;模型如果需要工具,就返回工具名、参数和调用 ID;宿主程序解析参数,按注册表找到真实执行器,完成 Schema、业务和权限校验后再调用;工具结果被规范化为 tool message,并通过 tool_call_id 与原请求关联;应用再次调用模型,让它基于 Observation 决定继续用工具还是输出最终答案。多轮重复后才形成 Agent loop。工程上我会额外处理空调用、未知工具、超时、结果过大、多工具依赖、写操作幂等和循环上限。评估也会拆成该不该调、工具选得对不对、参数是否合法、异常是否恢复、最终答案是否忠实使用结果,而不只看 Demo 能否跑通。
Function Calling 是否闭环:六项项目证据
- 能否画清模型、宿主程序、工具服务和用户四方职责。
- 是否保留 assistant tool call,并用调用 ID 回传 tool result。
- 工具参数是否经过 Schema、业务与权限三层校验。
- 超时、空结果、未知工具和无调用是否都有出口。
- 写操作是否有幂等、审批与审计,而不是模型说调就调。
- 是否有包含难负例和失败路径的评估集与 Trace。
只要始终记住一句话,这道题就不会答偏:模型负责说“我想调用什么”,你的程序负责判断“能不能调用、怎样调用”,工具结果回来后,模型才有资格继续回答。