大家好,我是大志。
本篇文章我结合自己的学习和项目经验,把工具调用相关的高频面试题整理成了一份比较完整的总结。不仅会介绍每个知识点是什么,还会结合实际开发场景,聊聊生产环境中常见的设计思路,希望能够帮助大家建立更加系统的知识体系。
另外,完整的 AI Agent 面试题文档也已经同步到了 aiflowline.cn,大家可以结合文章一起阅读。
1、什么是Function Calling?
Function Calling(函数调用)是LLM所具备的一种能力,函数调用是指模型不仅可以生成文本回答,还可以根据用户的提问,判断是否需要使用提供的工具,如需工具,就自动生成结构化参数,将工具调用信息返回给Agent程序,让Agent程序去调用外部工具(如外部API、数据库、搜索引擎等),最终将工具调用结果再返回给LLM来,LLM根据工具调用结果来判断是否还需要继续调用工具,还是直接返回结果。

工具调用的核心思想是让大模型不只是能回答问题,还能根据问题和提供的工具来决定要不要调用工具、调用哪个工具,以及如何传参,从而完成更多的任务。
例如用户问“上海今天的天气怎么样”,LLM的训练数据肯定不包括这类实时信息,LLM可能会直接编造答案,但是有了工具调用,LLM就会判断需要调用如get_weather工具,并生成类似 get_weather(city="上海") 这样的函数调用参数,返回给Agent执行工具。
Function Calling 的本质,其实是大模型与外部系统之间的一种结构化通信方式,让 LLM 从聊天机器人变成”可以使用工具的Agent智能体”,通过工具调用可以解决LLM无法获取实时信息、无法执行外部操作的问题。
2、如何设计Tool Schema?
Tool Schema 的核心目标是让LLM能够准确理解工具的用途、用法,并正确生成调用参数。因此,一个好的Tool Schema不仅是给程序看的,更重要的是给LLM看的。
在设计 Tool Schema 时,最重要的是写好 name、description 和 parameters。其中模型最依赖的其实是 description,因为模型并不知道我们的代码逻辑,它只能通过描述来理解这个工具是干什么的。除此之外,参数的设计也要尽量清晰、具体,避免让模型猜测。
在设计工具和Tool Schema时,要遵循单一职责原则,一个 Tool 最好只完成一类任务。
例如:
get_weather(city)search_news(keyword)而不是做一个万能工具,这会增加模型选择和参数生成的难度,降低调用成功率。
query_data( type, city, keyword, category, ...)工具名称也应该尽可能清晰明了,如果多个工具功能相似,需要在 Description 中明确区分边界。
例如:search_web、search_internal_docs,描述中要明确说明:一个搜索互联网,一个搜索企业知识库,否则LLM容易选错工具。
3、工具调用如何做参数校验?
参数校验的目的是确保 Agent 生成的工具调用参数符合要求,避免因为参数错误导致工具调用失败。
虽然 Function Calling 会根据 Tool Schema 生成参数,但大模型并不能保证生成的参数一定正确。例如用户输入异常、模型理解错误或者参数缺失时,都可能导致工具调用失败。因此在工具真正执行之前,必须进行参数校验。
以LangChain为例,通常会使用 Pydantic 定义参数模型:
from pydantic import BaseModel, FieldclassWeatherInput(BaseModel): city: str = Field( description="需要查询天气的城市名称" )然后绑定到 Tool:
@tool(args_schema=WeatherInput)defget_weather(city: str):returnf"{city}天气晴朗"当模型生成参数后:
{"city": "北京"}会先经过 Pydantic 校验,通过后才会执行工具。
如果模型生成:
{"city": 123}或者:
{}则会抛出异常,不会继续执行工具。除了类型校验之外,实际项目中还经常会做业务规则校验。例如:
from pydantic import field_validatorclassWeatherInput(BaseModel): city: str @field_validator("city") @classmethoddefvalidate_city(cls, value):ifnot value.strip():raise ValueError("城市不能为空")return value这样即使模型生成的city参数是空字符串,参数也会校验不通过。
4、工具调用失败如何处理?
在 Agent 应用中,工具调用失败非常常见,例如网络超时、API 限流、参数错误、数据库异常等。因此,工具调用需要设计完善的异常处理机制。
首先,要在 Tool 内部捕获异常,避免因为一个工具执行失败导致整个执行流程崩溃,让Agent仍然能够获取错误信息并继续运行。
@tooldefget_weather(city: str):try:return weather_api.query(city)except Exception as e:returnf"天气查询失败:{str(e)}"对于临时性故障,例如网络抖动、接口超时等问题,可以增加重试机制。
from tenacity import retry@retry(stop=stop_after_attempt(3))defget_weather(city: str): ...在Agent应用中,更推荐把错误信息返回给模型,而不是直接终止流程,模型收到错误后可以自主决策,比如返回给用户:天气查询服务暂时不可用,请稍后重试。
在复杂工作流中,还可以设计降级(Fallback)策略,例如主搜索工具Google Serper失败时,采用Bing Search继续进行搜索。
在生产环境中,还需要记录日志和监控,记录调用了哪个工具、输入参数是什么、错误原因是什么、重试次数是多少,这样方便后续排查问题和优化。
5、如何设计超时机制?
在 Agent 应用中,工具调用通常依赖第三方 API、数据库或搜索服务,这些服务可能出现请求超时的情况。因此需要设计超时机制,避免某个工具调用超时阻塞整个 Agent 流程。
最简单的做法是在调用外部服务时设置请求超时时间。例如使用 requests 调用接口时:
response = requests.get( url, timeout=10)如果 10 秒内没有返回结果,则抛出超时异常,交给模型去处理。
@tooldefget_weather(city: str):try:return weather_api.query(city)except TimeoutError:return"天气服务请求超时"模型获取到错误信息,并决定后续如何处理。
在一些复杂场景下,经常会使用降级策略。当主工具超时时,可以切换到备用工具,这样能够提升整体成功率。
6、如何设计重试机制?
在 Agent 应用中,工具调用失败并不一定意味着真正的业务失败,很多时候是因为网络抖动、接口超时、服务限流等原因。因此通常会设计重试机制,在失败后自动重新执行,来提高工具调用成功率。
最简单的方式是在 Tool 调用失败后重试固定次数。例如:
from tenacity import retry, stop_after_attempt@retry(stop=stop_after_attempt(3))defget_weather(city: str):return weather_api.query(city)当调用失败时,系统会自动重试,最多执行 3 次。
实际项目中,更推荐使用指数退避(Exponential Backoff)策略,而不是立即重试。
例如:
第1次失败 → 等待1秒第2次失败 → 等待2秒第3次失败 → 等待4秒这样可以避免在服务异常时持续发送大量请求,增加系统压力。而且,并不是所有错误都适合重试,需要区分错误类型。对于网络超时、服务暂时不可用、API 限流(429)、数据库连接失败可以尝试重试。
而对于参数错误、权限不足、资源不存在、数据格式错误这类错误,无需进行重试,大概率重试之后也不会成功。
并且重试也要控制重试次数,如最大重试次数3~5次,超过次数后直接返回错误信息,否则可能陷入死循环。
7、什么是指数退避重试?
指数退避(Exponential Backoff)是一种重试策略,每次重试的等待时间呈指数增长。
如:
第1次重试:等待 1秒第2次重试:等待 2秒第3次重试:等待 4秒第4次重试:等待 8秒第5次重试:等待 16秒等待时间 = 基础等待时间 * 2^(重试次数-1)
那么,为什么需要指数退避呢?如果服务端出了问题,大量客户端同时重试会导致服务端压力非常大,指数退避让客户端的重试间隔越来越长,能给服务端恢复的时间。
具体实现如下:
import timeimport randomdefexecute_with_backoff(tool_name, arguments, max_retries=5, base_delay=1):for attempt in range(max_retries):try:return execute_tool(tool_name, arguments)except RETRYABLE_ERRORS as e:if attempt == max_retries - 1:raise# 指数退避 + 随机抖动 delay = base_delay * (2 ** attempt) + random.uniform(0, 1) time.sleep(delay)纯指数退避可能导致多个客户端在同一时刻重试,加入0-1秒的随机抖动(Jitter)可以错开重试时间:
纯指数退避:1s, 2s, 4s, 8s(所有客户端相同)加入抖动后:1.3s, 2.1s, 4.7s, 8.2s(每个客户端根据随机抖动不同)8、如何设计Fallback机制?
Fallback是指当前工具不可用时,Agent自动切换到备选工具。
工具降级
同一个功能准备多个备选工具:
TOOL_FALLBACK = {"search_web": ["search_web_v2", "search_bing"],"get_weather": ["get_weather_api2"],}defexecute_with_fallback(tool_name, arguments): tools = [tool_name] + TOOL_FALLBACK.get(tool_name, [])for tool in tools:try:return execute_tool(tool, arguments)except Exception as e: logger.warning(f"工具{tool}调用失败,尝试其他工具: {e}")continueraise Exception(f"所有备选工具都失败: {tools}")这里要注意首选工具和备选工具参数和用法可能不完全相同,在发生降级调用时,要处理好这部分的逻辑。
策略级 Fallback
当工具调用全部失败时,降级到不需要工具的方案:
正常流程:用户问天气 → 调用天气API → 返回实时天气 降级流程:天气API调用失败 → 返回给用户"当前无法获取实时天气,建议您查看天气App"
缓存 Fallback
当调用工具失败之后,尝试使用缓存数据:
defexecute_with_cache_fallback(tool_name, arguments):# 先尝试实时调用try: result = execute_tool(tool_name, arguments) cache.set(tool_name, arguments, result, ttl=300) # 缓存5分钟return resultexcept Exception:# 实时调用失败,尝试从缓存获取 cached = cache.get(tool_name, arguments)if cached:return {"data": cached, "source": "cache", "warning": "使用了缓存数据"}raise使用缓存进行降级处理时,要结合具体业务场景,有些信息是必须要实时获取才有意义,避免获取到过期的信息。
9、如何避免Agent误调用工具?
Agent有时候会在不需要调用工具时调用工具,或者调用错误的工具。
在 Prompt中明确工具使用场景
工具使用规则:- 只有当用户明确要求执行操作时才调用工具- 如果用户只是在闲聊或问概念性问题,直接回答,不要调用工具- 不确定是否需要调用工具时,优先不调用高风险的工具调用进行人工确认
如下示例使用LangGraph来进行工具调用确认:
from typing import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.types import interrupt, Commandfrom langgraph.checkpoint.memory import InMemorySaverclassState(TypedDict): result: str# 高风险工具defdelete_database():return"数据库已删除"# 人工确认defhuman_review(state: State): approved = interrupt("是否允许删除数据库?")return {"approved": approved}# 根据审批结果路由defrouter(state):return"tool"if state["approved"] else"reject"# 执行工具deftool(state):return {"result": delete_database()}# 拒绝执行defreject(state):return {"result": "已取消操作"}builder = StateGraph(State)builder.add_node("review", human_review)builder.add_node("tool", tool)builder.add_node("reject", reject)builder.add_edge(START, "review")builder.add_conditional_edges("review", router)builder.add_edge("tool", END)builder.add_edge("reject", END)graph = builder.compile(checkpointer=InMemorySaver())人工确认
graph.invoke(Command(resume=True), config=config)工具描述中写明边界
如在编写发送邮件send_email时,添加描述:发送邮件。仅在用户明确要求发送邮件时调用,不要在用户只是询问邮件相关问题时调用。
减少工具数量
工具越多,LLM选错的概率越大。定期清理不常用的工具,保持工具列表精简。
10、如何限制Agent调用危险工具?
某些工具(如删除数据、执行代码、发送邮件)具有不可逆的影响,需要严格限制。
权限分级
TOOL_RISK_LEVEL = {"search_web": "low", # 只读操作,风险低"send_email": "medium", # 有副作用,中等风险"delete_file": "high", # 不可逆,高风险"execute_code": "critical"# 可能造成严重后果}defcheck_tool_permission(tool_name, user_role): risk = TOOL_RISK_LEVEL.get(tool_name, "unknown")if risk == "critical":return user_role == "admin"if risk == "high":return user_role in ["admin", "operator"]returnTrue二次确认
高风险操作必须经过人工确认,具体示例如上面所示的删除数据二次确认的案例。
日志审计
记录所有高风险工具的调用日志,方便排查问题和回滚。
11、工具执行异常如何回滚?
当工具执行到一半失败了,是否需要回滚,需要根据工具具体进行判断,查询类工具通常不需要回滚;而创建订单、扣库存、删除数据等写操作,如果后续流程失败,就应该考虑补偿机制。
由于 Agent 会跨数据库、HTTP API、第三方服务等多个系统,通常无法依赖传统数据库事务,而是采用 Saga 或补偿事务模式,为每个关键工具设计对应的回滚操作。
事务性设计
尽量让工具调用支持事务,失败时自动回滚:
defexecute_with_transaction(operations): executed = []try:for op in operations: result = execute_tool(op["tool"], op["arguments"]) executed.append({"tool": op["tool"], "result": result})return {"success": True, "results": executed}except Exception as e:# 逆序回滚已成功执行的操作for op in reversed(executed): rollback(op["tool"], op["result"])return {"success": False, "error": str(e)}补偿操作
如果不能回滚,设计补偿操作来撤销影响:
COMPENSATION_MAP = {"send_email": "send_recall_email","create_order": "cancel_order","transfer_money": "reverse_transfer",}defcompensate(tool_name, result): compensation_tool = COMPENSATION_MAP.get(tool_name)if compensation_tool: execute_tool(compensation_tool, {"original_result": result})标记待确认
对于无法回滚的操作,先标记为"待确认"状态,等待后续处理。
defexecute_with_pending(tool_name, arguments): result = execute_tool(tool_name, arguments)# 标记为待确认状态 mark_as_pending(result["id"])return resultdefconfirm_execution(result_id): mark_as_confirmed(result_id)12、如何记录工具调用日志?
完善的日志记录是排查问题和优化Agent的基础。
(1)日志内容
每次工具调用需要记录以下信息:
deflog_tool_call(tool_name, arguments, result, duration, error=None): log_entry = {"timestamp": datetime.now().isoformat(),"tool_name": tool_name,"arguments": arguments,"result_summary": str(result)[:200], # 只记录摘要,避免日志过大"duration_ms": duration,"success": error isNone,"error": str(error) if error elseNone,"session_id": get_session_id(),"user_id": get_user_id() } logger.info(json.dumps(log_entry, ensure_ascii=False))(2)敏感信息脱敏
日志中可能包含敏感信息,需要脱敏处理:
defmask_sensitive(arguments): masked = arguments.copy() sensitive_keys = ["password", "token", "card_number", "id_card"]for key in sensitive_keys:if key in masked: masked[key] = "***"return masked(3)日志分级
# 普通工具调用 → INFO级别logger.info(f"工具调用成功: {tool_name}")# 工具调用失败 → WARNING级别logger.warning(f"工具调用失败: {tool_name}, 错误: {error}")# 高风险工具调用 → 单独记录审计日志audit_logger.info(f"高风险操作: {tool_name}, 用户: {user_id}")13、如何追踪工具调用链路?
一个Agent任务可能包含多次工具调用,需要把它们串联起来形成完整的调用链路。
(1)Trace ID
给每个Agent任务分配一个唯一的Trace ID,所有相关的工具调用都带上这个ID:
import uuidclassToolCallTracer:def__init__(self): self.trace_id = str(uuid.uuid4()) self.spans = []defstart_span(self, tool_name, arguments): span = {"trace_id": self.trace_id,"span_id": str(uuid.uuid4()),"tool_name": tool_name,"arguments": arguments,"start_time": time.time(),"status": "running" } self.spans.append(span)return span["span_id"]defend_span(self, span_id, result=None, error=None):for span in self.spans:if span["span_id"] == span_id: span["end_time"] = time.time() span["duration"] = span["end_time"] - span["start_time"] span["result"] = result span["error"] = error span["status"] = "success"if error isNoneelse"error"break(2)调用链路可视化
把追踪数据可视化,方便排查问题:
Trace: abc-123├── [0.0s] search_web(query="AI新闻")│ └── [2.3s] 成功,返回10条结果├── [2.5s] summarize(text=搜索结果)│ └── [5.1s] 成功,生成摘要└── [5.3s] send_email(to="team@example.com", content=摘要) └── [6.8s] 成功,邮件已发送总耗时:6.8秒(3)关联LLM调用
除了工具调用,LLM的调用也需要追踪,形成完整的调用链路:
完整调用链路:├── LLM调用 #1 → 决定调用search_web├── Tool调用 search_web → 返回搜索结果├── LLM调用 #2 → 决定调用summarize├── Tool调用 summarize → 返回摘要├── LLM调用 #3 → 决定调用send_email├── Tool调用 send_email → 发送成功└── LLM调用 #4 → 生成最终回答总耗时:20.3秒好啦,今天这期 工具调用 面试题就到这里。后面我会每周至少更新1期面试题系列,想看后续 【AI Agent 进阶面试题】 的朋友,欢迎关注「大志说编程」!
觉得有用的话,转发给正在面试的小伙伴,咱们下期见~
往期回顾