大家好,我是大志。
本篇文章我结合自己的学习和项目经验,把工具调用相关的高频面试题整理成了一份比较完整的总结。不仅会介绍每个知识点是什么,还会结合实际开发场景,聊聊生产环境中常见的设计思路,希望能够帮助大家建立更加系统的知识体系。
另外,完整的 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, Field
class WeatherInput(BaseModel):
city: str = Field(
description="需要查询天气的城市名称"
)然后绑定到 Tool:
@tool(args_schema=WeatherInput)
def get_weather(city: str):
return f"{city}天气晴朗"当模型生成参数后:
{
"city": "北京"
}会先经过 Pydantic 校验,通过后才会执行工具。
如果模型生成:
{
"city": 123
}或者:
{}则会抛出异常,不会继续执行工具。除了类型校验之外,实际项目中还经常会做业务规则校验。例如:
from pydantic import field_validator
class WeatherInput(BaseModel):
city: str
@field_validator("city")
@classmethod
def validate_city(cls, value):
if not value.strip():
raise ValueError("城市不能为空")
return value这样即使模型生成的city参数是空字符串,参数也会校验不通过。
4、工具调用失败如何处理?
在 Agent 应用中,工具调用失败非常常见,例如网络超时、API 限流、参数错误、数据库异常等。因此,工具调用需要设计完善的异常处理机制。
首先,要在 Tool 内部捕获异常,避免因为一个工具执行失败导致整个执行流程崩溃,让Agent仍然能够获取错误信息并继续运行。
@tool
def get_weather(city: str):
try:
return weather_api.query(city)
except Exception as e:
return f"天气查询失败:{str(e)}"对于临时性故障,例如网络抖动、接口超时等问题,可以增加重试机制。
from tenacity import retry
@retry(stop=stop_after_attempt(3))
def get_weather(city: str):
...在Agent应用中,更推荐把错误信息返回给模型,而不是直接终止流程,模型收到错误后可以自主决策,比如返回给用户:天气查询服务暂时不可用,请稍后重试。
在复杂工作流中,还可以设计降级(Fallback)策略,例如主搜索工具Google Serper失败时,采用Bing Search继续进行搜索。
在生产环境中,还需要记录日志和监控,记录调用了哪个工具、输入参数是什么、错误原因是什么、重试次数是多少,这样方便后续排查问题和优化。
5、如何设计超时机制?
在 Agent 应用中,工具调用通常依赖第三方 API、数据库或搜索服务,这些服务可能出现请求超时的情况。因此需要设计超时机制,避免某个工具调用超时阻塞整个 Agent 流程。
最简单的做法是在调用外部服务时设置请求超时时间。例如使用 requests 调用接口时:
response = requests.get(
url,
timeout=10
)如果 10 秒内没有返回结果,则抛出超时异常,交给模型去处理。
@tool
def get_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))
def get_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 time
import random
def execute_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"],
}
def execute_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}")
continue
raise Exception(f"所有备选工具都失败: {tools}")这里要注意首选工具和备选工具参数和用法可能不完全相同,在发生降级调用时,要处理好这部分的逻辑。
- 策略级
Fallback
当工具调用全部失败时,降级到不需要工具的方案:
正常流程:用户问天气 → 调用天气API → 返回实时天气 降级流程:天气API调用失败 → 返回给用户"当前无法获取实时天气,建议您查看天气App"
- 缓存
Fallback
当调用工具失败之后,尝试使用缓存数据:
def execute_with_cache_fallback(tool_name, arguments):
# 先尝试实时调用
try:
result = execute_tool(tool_name, arguments)
cache.set(tool_name, arguments, result, ttl=300) # 缓存5分钟
return result
except Exception:
# 实时调用失败,尝试从缓存获取
cached = cache.get(tool_name, arguments)
if cached:
return {"data": cached, "source": "cache", "warning": "使用了缓存数据"}
raise使用缓存进行降级处理时,要结合具体业务场景,有些信息是必须要实时获取才有意义,避免获取到过期的信息。
9、如何避免Agent误调用工具?
Agent有时候会在不需要调用工具时调用工具,或者调用错误的工具。
- 在
Prompt中明确工具使用场景
工具使用规则:
- 只有当用户明确要求执行操作时才调用工具
- 如果用户只是在闲聊或问概念性问题,直接回答,不要调用工具
- 不确定是否需要调用工具时,优先不调用- 高风险的工具调用进行人工确认
如下示例使用LangGraph来进行工具调用确认:
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
class State(TypedDict):
result: str
# 高风险工具
def delete_database():
return "数据库已删除"
# 人工确认
def human_review(state: State):
approved = interrupt("是否允许删除数据库?")
return {"approved": approved}
# 根据审批结果路由
def router(state):
return "tool" if state["approved"] else "reject"
# 执行工具
def tool(state):
return {"result": delete_database()}
# 拒绝执行
def reject(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" # 可能造成严重后果
}
def check_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"]
return True- 二次确认
高风险操作必须经过人工确认,具体示例如上面所示的删除数据二次确认的案例。
- 日志审计
记录所有高风险工具的调用日志,方便排查问题和回滚。
11、工具执行异常如何回滚?
当工具执行到一半失败了,是否需要回滚,需要根据工具具体进行判断,查询类工具通常不需要回滚;而创建订单、扣库存、删除数据等写操作,如果后续流程失败,就应该考虑补偿机制。
由于 Agent 会跨数据库、HTTP API、第三方服务等多个系统,通常无法依赖传统数据库事务,而是采用 Saga 或补偿事务模式,为每个关键工具设计对应的回滚操作。
- 事务性设计
尽量让工具调用支持事务,失败时自动回滚:
def execute_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",
}
def compensate(tool_name, result):
compensation_tool = COMPENSATION_MAP.get(tool_name)
if compensation_tool:
execute_tool(compensation_tool, {"original_result": result})- 标记待确认
对于无法回滚的操作,先标记为"待确认"状态,等待后续处理。
def execute_with_pending(tool_name, arguments):
result = execute_tool(tool_name, arguments)
# 标记为待确认状态
mark_as_pending(result["id"])
return result
def confirm_execution(result_id):
mark_as_confirmed(result_id)12、如何记录工具调用日志?
完善的日志记录是排查问题和优化Agent的基础。
(1)日志内容
每次工具调用需要记录以下信息:
def log_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 is None,
"error": str(error) if error else None,
"session_id": get_session_id(),
"user_id": get_user_id()
}
logger.info(json.dumps(log_entry, ensure_ascii=False))(2)敏感信息脱敏
日志中可能包含敏感信息,需要脱敏处理:
def mask_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 uuid
class ToolCallTracer:
def __init__(self):
self.trace_id = str(uuid.uuid4())
self.spans = []
def start_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"]
def end_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 is None else "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 进阶面试题】 的朋友,欢迎关注「大志说编程」!
觉得有用的话,转发给正在面试的小伙伴,咱们下期见~