1. 项目概述:为什么我们需要 Agent?
最近和不少刚接触 AI 应用开发的朋友聊天,发现一个挺普遍的现象:大家一上来就兴致勃勃地讨论要搞个“智能 Agent”,但细问之下,很多人其实没太搞明白 Agent 和它底层的 LLM(大语言模型)到底有啥本质区别。这感觉就像是想造一辆能自动驾驶的汽车,却把全部精力都放在了研究发动机的马力上,忽略了方向盘、传感器和决策系统。结果往往是,用上了最先进的“发动机”(比如 GPT-4),做出来的东西却像个“人工智障”,只能一问一答,离真正的“智能体”相去甚远。
所以,今天我想从一个一线开发者的角度,掰开揉碎了聊聊这个话题。我们不谈那些高大上的学术定义,就说说在实际项目中,LLM 和 Agent 到底扮演什么角色,以及那个听起来有点技术范儿的Function Calling,是如何成为连接两者的关键桥梁的。无论你是想自己动手搭建一个能处理复杂任务的 AI 助手,还是仅仅想理解当前 AI 应用的技术脉络,这篇文章都会给你一个清晰、可操作的认知框架。
简单来说,你可以把LLM 看作是一个“超级大脑”,它博览群书(训练数据),知识渊博,擅长理解和生成自然语言。你问它“今天天气怎么样?”,它能根据训练数据里的知识,编造一段合情合理的描述。但它有个致命缺陷:它活在“虚拟世界”里,它的“知识”截止于训练数据,它无法感知实时数据(比如此刻的真实天气),也无法操作外部系统(比如帮你订一张机票)。它的回答,是基于概率的“语言模仿”。
而Agent(智能体)则是一个“完整的智能个体”。它同样拥有一个“大脑”(通常是 LLM),但这个大脑被赋予了“感知器官”(能获取实时信息)、“执行器官”(能调用工具完成任务)和“记忆系统”(能记住对话历史和任务上下文)。更重要的是,它有一套“思考逻辑”(Agent 框架),能自主规划、决策、使用工具,最终达成一个复杂目标。比如,你让一个旅行 Agent “帮我规划一个周末去杭州的行程,并预订高铁票和酒店”,它会自己分解任务:先调用天气 API 查杭州天气,再调用交通 API 查高铁班次,接着调用酒店预订 API 筛选酒店,最后把结果整合成一份行程单给你。这个过程,LLM 只是其中负责“思考规划”和“语言组织”的组件。
那么,LLM 这个“虚拟大脑”,是如何指挥“物理世界”的工具去执行任务的呢?答案就是Function Calling(函数调用)。这是当前构建实用 Agent 最核心、最主流的技术范式。它本质上是一种“标准化协议”,让 LLM 能够以结构化的方式“表达意图”:当它判断需要执行某个外部操作时,不是用模糊的自然语言说“你去查一下天气”,而是输出一个格式严格的 JSON 对象,指明要调用哪个函数(工具),并传入哪些参数。外部的执行引擎收到这个 JSON,就去真正执行对应的代码(如调用天气 API),然后把执行结果(结构化的数据)再塞回给 LLM,由 LLM 组织成人类友好的语言输出。
理解了这三者的关系,你就能明白,从 LLM 到 Agent 的跨越,关键在于从“语言生成”到“目标驱动行动”的转变。而 Function Calling 就是实现这一转变的“关节”技术。接下来,我们就深入这个“关节”,看看它具体是怎么工作的,以及在实践中如何用好它。
2. 核心原理拆解:Function Calling 如何让 LLM“动手做事”
2.1 从“聊天”到“调度”:思维模式的根本转变
在没有 Function Calling 之前,我们和 LLM 的交互基本是“聊天模式”。用户输入一个问题,模型输出一段回答。即使我们通过复杂的提示工程(Prompt Engineering)让模型在回答中“暗示”需要某个信息,比如“要回答这个问题,我需要知道北京的实时气温”,但这也只是一段文本。应用程序需要非常脆弱且不稳定的文本解析(比如正则表达式)来尝试捕捉这种意图,然后手动去调用 API,再把结果拼接回对话上下文。这个过程笨重、易错,且很难扩展。
Function Calling 引入了一种“声明式”的交互范式。我们不再要求模型“在回答里暗示”,而是提前告诉模型:“伙计,我这里给你准备了几个工具,这是它们的功能说明和用法。当你觉得需要用到时,就直接告诉我你要用哪个,参数是什么。” 模型输出的不再是最终答案的自然语言,而是一个或多个标准的“工具调用请求”。
这个转变的核心在于“将意图识别与结构化输出相结合”。LLM 擅长理解用户的自然语言意图(“我想知道天气”),而 Function Calling 要求它把这种意图,映射到我们预先定义好的、结构化的工具调用指令上。这相当于给 LLM 的“思考过程”加了一个输出模板,让它从天马行空的文本生成,转变为目标明确的“调度指令”生成。
2.2 技术实现剖析:OpenAI 格式的 Function Calling
目前,业界最广泛采用的 Function Calling 标准源自 OpenAI API。我们以其为例,拆解其核心组成部分。当你向 ChatGPT 或相关 API 发起一个包含工具定义的请求时,流程如下:
1. 工具定义(告诉模型你有什么)你需要在请求中,以 JSON Schema 的形式,向模型描述你可用的工具(函数)。每个工具定义包括:
name: 函数名称,如get_current_weather。description: 函数功能的自然语言描述。这部分至关重要,它直接决定了模型是否能在合适的场景下想起并使用这个工具。描述应清晰、简洁,说明函数做什么、适用于什么场景。parameters: 函数的参数列表,同样用 JSON Schema 描述。包括参数名、类型、描述,以及是否是必需的。
{ "type": "function", "function": { "name": "get_current_weather", "description": "获取指定城市的当前天气情况", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "城市名称,例如:北京,上海" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位,摄氏度或华氏度" } }, "required": ["location"] } } }2. 模型决策与结构化输出(模型选择工具)用户提问:“北京今天热吗?” 模型结合对话上下文和你提供的工具定义,进行推理。它会想:“用户问北京热不热,这需要天气信息。我有个工具叫get_current_weather可以获取天气,而且需要location参数。北京符合这个参数。” 于是,模型不会生成“北京今天气温是...”,而是生成一个特殊的消息响应,其content字段可能为空,但会包含一个tool_calls数组。
{ "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"北京\", \"unit\": \"celsius\"}" } } ] }注意,arguments是一个 JSON 字符串,其结构必须完全符合之前定义的parametersSchema。
3. 执行与回传(应用程序干活)你的应用程序收到这个响应后,解析tool_calls。根据name找到本地真正的函数get_current_weather,解析arguments中的参数,然后执行它(比如调用一个真实的天气 API)。执行完毕后,你需要将结果以特定格式追加到对话历史中。
4. 结果整合与最终回复(模型生成答案)你将工具执行的结果,作为一个新的消息,role设为tool,送回给模型。
{ "role": "tool", "content": "{\"temperature\": 28, \"condition\": \"晴朗\", \"humidity\": 65}", "tool_call_id": "call_abc123" }模型看到这个工具执行结果后,结合最初的用户问题,生成最终的自然语言回复:“北京今天天气晴朗,气温28摄氏度,比较暖和。”
实操心得:描述(description)是灵魂很多新手在定义工具时,只草草写个名字和参数类型,结果发现模型经常“瞎用”或“不用”。关键在于
description和参数description。要用模型能理解的自然语言,清晰说明工具的用途、适用场景以及每个参数的确切含义。例如,location的描述写成“城市或地区名”,就比“地点”要好。这本质上是你在“教”模型如何理解和使用这个工具。
2.3 与其他技术路径的对比
除了 Function Calling,早期让 LLM 使用工具还有别的方法,了解它们有助于理解 Function Calling 的优势:
- ReAct(Reasoning + Acting)模式:通过精心设计的提示词,要求模型以“Thought: ... Action: ... Observation: ...”的格式进行链式思考。Action 部分输出类似
Search[关键字]的文本,再由程序解析执行。这种方式完全依赖提示工程和模型的文本遵循能力,格式不稳定,解析容易出错,开发体验较差。 - LangChain Tools:LangChain 框架封装了工具调用的通用流程,其底层早期也依赖 ReAct 等模式,现在也深度集成了 OpenAI 的 Function Calling。它提供了更高层次的抽象,但理解其底层原理(即 Function Calling)对于调试和优化至关重要。
Function Calling 的核心优势在于“标准化”和“结构化”。它将工具调用的意图、参数和结果都约束在明确的 JSON Schema 内,极大降低了系统集成的复杂度,提高了可靠性和开发效率。它已经成为当前 AI 应用开发中,连接 LLM 认知世界与外部行动世界的“事实标准”。
3. 从零构建一个简易天气查询 Agent
理论说再多,不如动手做一遍。我们用一个最简单的“天气查询 Agent”为例,展示如何将 LLM(这里用 OpenAI GPT)与 Function Calling 结合,打造一个能真正“动手”查天气的智能体。我们将使用 Python 和 OpenAI SDK,但思路适用于任何语言。
3.1 环境准备与工具函数定义
首先,确保你已安装 openai 库并配置好 API 密钥。
pip install openai接下来,我们定义这个 Agent 的核心——工具函数。这个函数并不直接调用真实 API(为了简化),而是模拟返回数据。
import json import openai from typing import Optional # 设置你的 OpenAI API 密钥 openai.api_key = 'your-api-key-here' # 模拟的天气数据源 def get_current_weather(location: str, unit: str = "celsius") -> str: """ 模拟获取天气的函数。 在实际应用中,这里会调用如 OpenWeatherMap、和风天气等第三方 API。 Args: location: 城市名称,如“北京”、“上海”。 unit: 温度单位,“celsius” 或 “fahrenheit”。 Returns: 返回一个包含天气信息的 JSON 字符串。 """ # 模拟根据城市返回数据 weather_data = { "北京": {"temperature": 22, "condition": "多云", "humidity": 60}, "上海": {"temperature": 28, "condition": "晴朗", "humidity": 75}, "广州": {"temperature": 32, "condition": "雷阵雨", "humidity": 85}, } data = weather_data.get(location, {"temperature": 25, "condition": "未知", "humidity": 50}) # 模拟单位转换(简陋版) if unit == "fahrenheit": data["temperature"] = data["temperature"] * 9/5 + 32 return json.dumps(data) # 定义工具的 Schema,用于告诉模型这个工具的存在和用法 tools = [ { "type": "function", "function": { "name": "get_current_weather", "description": "获取指定城市的当前天气信息,包括温度、天气状况和湿度。", # 清晰描述 "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "需要查询天气的城市名称,必须是一个明确的城市名,例如:北京、纽约、伦敦。", # 参数描述具体化 }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度的单位,默认为摄氏度(celsius)。", } }, "required": ["location"], # 指定必需参数 }, } } ]注意事项:工具函数的纯文本化注意,
get_current_weather函数返回的是一个 JSON 字符串,而不是 Python 字典。这是因为在后续步骤中,我们需要将这个结果作为content字段(必须是字符串)传回给模型。这是一种常见的模式:工具函数处理外部世界,返回结构化的字符串数据(通常是 JSON),由模型来解读并生成自然语言。
3.2 构建对话循环与工具调用逻辑
核心逻辑在于一个循环:维护对话历史,每次将用户消息和工具定义发给模型,检查模型是否要求调用工具,如果是则执行工具并将结果追加到历史,然后再次请求模型生成最终回答。
def run_weather_agent(): """ 运行一个简单的天气查询 Agent。 """ # 初始化对话历史 messages = [ {"role": "system", "content": "你是一个友好的天气助手。请根据用户的需求,使用工具查询天气并给出回答。如果用户的问题不涉及天气,请礼貌告知。"} ] print("天气助手已启动。输入‘退出’或‘quit’结束对话。") while True: # 1. 获取用户输入 user_input = input("\n你:") if user_input.lower() in ["退出", "quit", "exit"]: print("助手:再见!") break # 2. 将用户输入加入对话历史 messages.append({"role": "user", "content": user_input}) # 3. 首次调用模型,允许其选择工具 try: response = openai.chat.completions.create( model="gpt-3.5-turbo", # 或 "gpt-4" messages=messages, tools=tools, # 关键:传入工具定义 tool_choice="auto", # “auto”表示由模型决定是否调用工具 ) except Exception as e: print(f"调用API出错:{e}") break # 4. 处理模型响应 response_message = response.choices[0].message messages.append(response_message) # 将助手的响应(可能包含工具调用)也加入历史 # 5. 检查模型是否调用了工具 tool_calls = response_message.tool_calls if tool_calls: print(f"助手:我需要查询天气信息...") # 可能存在多个工具调用(并行),这里我们循环处理 for tool_call in tool_calls: function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) # 6. 执行对应的工具函数 if function_name == "get_current_weather": location = function_args.get("location") unit = function_args.get("unit", "celsius") # 在实际应用中,这里应该有更完善的错误处理 if not location: print(f"错误:未提供城市参数。") function_response = json.dumps({"error": "Missing location parameter"}) else: function_response = get_current_weather(location, unit) else: function_response = json.dumps({"error": f"未知工具:{function_name}"}) # 7. 将工具执行结果作为一条新消息追加到历史 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": function_response, }) # 8. 第二次调用模型,让它基于工具执行结果生成最终回答 second_response = openai.chat.completions.create( model="gpt-3.5-turbo", messages=messages, # 此时历史包含了工具执行结果 ) final_message = second_response.choices[0].message messages.append(final_message) # 将最终回答也加入历史,保持上下文完整 print(f"助手:{final_message.content}") else: # 模型没有调用工具,直接输出回答 print(f"助手:{response_message.content}")3.3 运行示例与过程解析
运行上面的run_weather_agent()函数,让我们看看一次完整的交互流程。
天气助手已启动。输入‘退出’或‘quit’结束对话。 你:上海今天天气怎么样? 助手:我需要查询天气信息... 助手:上海今天天气晴朗,气温28摄氏度,湿度75%,感觉会比较暖和。 你:那北京呢?用华氏度告诉我。 助手:我需要查询天气信息... 助手:北京今天的天气是多云,气温大约是72华氏度(对应22摄氏度),湿度为60%。过程拆解:
- 用户输入:“上海今天天气怎么样?”
- 模型第一次响应:模型收到消息和工具定义。它分析问题,识别出需要天气信息,且城市是“上海”。于是,它决定调用
get_current_weather工具,并生成一个tool_calls响应,其中arguments为{"location": "上海", "unit": "celsius"}(默认单位)。此时它不生成最终答案。 - 程序执行工具:我们的代码解析出工具调用,执行本地的
get_current_weather("上海", "celsius")函数,得到模拟结果{"temperature": 28, "condition": "晴朗", "humidity": 75}。 - 回传结果:程序将上述结果以
role: tool的消息格式追加到对话历史。 - 模型第二次响应:模型看到工具执行返回的具体数据(温度28,晴朗,湿度75),结合用户最初的问题“上海今天天气怎么样?”,生成最终的自然语言回答:“上海今天天气晴朗,气温28摄氏度,湿度75%,感觉会比较暖和。”
- 后续对话:当用户问“那北京呢?用华氏度告诉我。”时,对话历史中已经包含了之前的全部交互。模型能理解“北京”指代地点,“用华氏度”是新的单位要求。它再次发起工具调用,参数为
{"location": "北京", "unit": "fahrenheit"},程序执行后返回华氏度数据,模型最终整合回答。
实操心得:对话历史的管理是关键这个简易示例中,我们手动维护
messages列表。在复杂 Agent 中,对话历史管理(Context Management)是个大学问。你需要决定保留多少轮历史(token 有限),如何对历史进行总结压缩,以及如何确保工具调用和结果被正确关联。一个常见的坑是忘记将tool角色的消息(执行结果)传回给模型,导致模型“失忆”,不知道工具执行是否成功。
4. 进阶实战:构建多工具、可规划的旅行规划 Agent
单一工具的 Agent 只是个开始。真正的价值在于让 Agent 能自主规划、按顺序或并行使用多个工具来完成复杂目标。我们升级一下场景,构建一个“旅行规划助手”,它能调用多个工具:查询天气、查询航班(模拟)、查询酒店(模拟)。
4.1 设计工具集与系统提示词
首先,我们定义更丰富的工具集。为了模拟,我们创建三个工具函数。
# 模拟工具函数 def search_flights(departure: str, arrival: str, date: str) -> str: """模拟查询航班信息。""" # 模拟数据 flights = [ {"airline": "东方航空", "flight_no": "MU5101", "departure_time": "08:00", "arrival_time": "10:15", "price": 1200}, {"airline": "中国国航", "flight_no": "CA1501", "departure_time": "14:30", "arrival_time": "16:45", "price": 1100}, ] return json.dumps({"flights": flights, "query": {"departure": departure, "arrival": arrival, "date": date}}) def search_hotels(location: str, check_in: str, check_out: str) -> str: """模拟查询酒店信息。""" hotels = [ {"name": "西湖宾馆", "star": 4, "price_per_night": 600, "rating": 4.5}, {"name": "杭州国际青年旅舍", "star": 2, "price_per_night": 150, "rating": 4.2}, ] return json.dumps({"hotels": hotels, "query": {"location": location, "check_in": check_in, "check_out": check_out}}) # get_current_weather 函数沿用之前的 # 定义多工具 Schema travel_tools = [ { "type": "function", "function": { "name": "get_current_weather", "description": "获取指定城市的当前天气情况,用于旅行前的天气参考。", "parameters": { "type": "object", "properties": { "location": {"type": "string", "description": "城市名"}, }, "required": ["location"], }, } }, { "type": "function", "function": { "name": "search_flights", "description": "查询指定日期、出发地和目的地的航班信息。", "parameters": { "type": "object", "properties": { "departure": {"type": "string", "description": "出发城市机场代码或名称,如‘北京’、‘PEK’。"}, "arrival": {"type": "string", "description": "到达城市机场代码或名称,如‘杭州’、‘HGH’。"}, "date": {"type": "string", "description": "出发日期,格式 YYYY-MM-DD。"}, }, "required": ["departure", "arrival", "date"], }, } }, { "type": "function", "function": { "name": "search_hotels", "description": "查询指定城市、入住和离店日期的酒店信息。", "parameters": { "type": "object", "properties": { "location": {"type": "string", "description": "酒店所在城市"}, "check_in": {"type": "string", "description": "入住日期,格式 YYYY-MM-DD"}, "check_out": {"type": "string", "description": "离店日期,格式 YYYY-MM-DD"}, }, "required": ["location", "check_in", "check_out"], }, } } ]接下来,设计一个强大的系统提示词(System Prompt),这是 Agent 的“人格”和“行为准则”。一个好的系统提示词能显著提升 Agent 的规划能力和可靠性。
system_prompt = """ 你是一个专业的旅行规划助手。你的目标是帮助用户规划一次完整的旅行。 请遵循以下步骤和原则: 1. **明确需求**:首先与用户沟通,明确旅行的核心要素:目的地、出发地、旅行日期、人数、预算、兴趣点等。如果信息不全,主动询问。 2. **分步规划**:规划通常按逻辑顺序进行:先确定目的地和日期 -> 查询目的地天气作为参考 -> 查询往返交通(航班/高铁)-> 查询住宿。 3. **善用工具**:你有三个工具:`get_current_weather`、`search_flights`、`search_hotels`。根据当前规划步骤,选择合适的工具。一次可以调用一个或多个工具。 4. **信息整合**:获得工具返回的数据后,以清晰、有条理的方式向用户汇报,并可以给出初步建议(如“上午的航班价格更优”、“这家酒店评分很高”)。 5. **持续交互**:根据用户的反馈,调整规划或进行下一步。例如,用户对某个航班不满意,你可以重新查询其他航班。 记住,你是助手,最终决定权在用户。保持友好、专业、乐于助人。 """4.2 实现带规划逻辑的 Agent 引擎
现在,我们实现一个更健壮的 Agent 循环,它能处理多轮对话、多次工具调用,并整合系统提示词。
def run_travel_agent(): messages = [{"role": "system", "content": system_prompt}] print("=== 旅行规划助手启动 ===") print("我可以帮你查询天气、航班和酒店信息来规划旅行。请告诉我你的需求。") while True: user_input = input("\n你:") if user_input.lower() in ["退出", "quit"]: break messages.append({"role": "user", "content": user_input}) # 我们允许模型在单轮对话中多次调用工具(通过循环) # 设置一个最大轮次防止死循环 max_tool_rounds = 5 for round_num in range(max_tool_rounds): # 调用模型 response = openai.chat.completions.create( model="gpt-3.5-turbo", messages=messages, tools=travel_tools, tool_choice="auto", ) response_message = response.choices[0].message messages.append(response_message) # 检查是否有工具调用 if not response_message.tool_calls: # 没有工具调用,直接输出并跳出工具调用循环 print(f"助手:{response_message.content}") break # 处理工具调用 print(f"助手:[正在查询信息...]") for tool_call in response_message.tool_calls: func_name = tool_call.function.name func_args = json.loads(tool_call.function.arguments) # 根据工具名分派执行 if func_name == "get_current_weather": result = get_current_weather(**func_args) elif func_name == "search_flights": result = search_flights(**func_args) elif func_name == "search_hotels": result = search_hotels(**func_args) else: result = json.dumps({"error": f"Function {func_name} not found."}) # 将结果追加到消息历史 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) # 如果这是最后一轮工具调用循环,需要再请求一次模型生成最终回答 if round_num == max_tool_rounds - 1: final_response = openai.chat.completions.create( model="gpt-3.5-turbo", messages=messages, ) final_message = final_response.choices[0].message messages.append(final_message) print(f"助手:{final_message.content}") break # 否则,继续循环,让模型基于新的工具结果决定下一步动作 else: # 如果循环正常结束(非break),说明达到了最大工具调用轮次 print("助手:查询步骤较多,我们稍后再继续。")4.3 复杂场景测试与 Agent 行为分析
让我们运行这个 Agent,看它如何处理一个多步骤的复杂请求。
=== 旅行规划助手启动 === 我可以帮你查询天气、航班和酒店信息来规划旅行。请告诉我你的需求。 你:我想下周末从北京去杭州玩两天。 助手:[正在查询信息...] 助手:好的,我来帮你规划。首先,我们需要明确一下具体日期。“下周末”是指具体哪两天呢?比如是6月15-16日(周六日)吗?另外,你们有几个人出行,预算大概是多少?这样我可以为你查找更合适的航班和酒店。 你:就一个人,预算一般吧。日期是6月15到16号。 助手:[正在查询信息...] 助手:[正在查询信息...] 助手:好的,基于你的需求(北京到杭州,6月15-16日,一人),我为你查询了相关信息: 1. **天气参考**:杭州当前的天气情况是【模拟数据】,出行前建议再确认临近日期的预报。 2. **航班信息**(6月15日出发): * 东方航空 MU5101, 08:00 - 10:15, 价格 1200元 * 中国国航 CA1501, 14:30 - 16:45, 价格 1100元 国航的航班下午出发,价格稍低。如果你希望有更多白天时间在杭州,可以选择早上的航班。 3. **酒店信息**(6月15日入住,16日离店): * 西湖宾馆, 四星级, 600元/晚, 评分4.5 * 杭州国际青年旅舍, 二星级, 150元/晚, 评分4.2 西湖宾馆位于西湖附近,位置和设施更好;青年旅舍则性价比极高。 这是一个初步方案。你对航班时间或酒店类型有偏好吗?我可以进一步筛选或查询其他选项。行为分析:
- 需求澄清:模型首先没有盲目调用工具,而是根据系统提示词,识别出“下周末”是模糊信息,主动询问具体日期、人数和预算。这体现了 Agent 的“规划”和“交互”能力。
- 并行工具调用:在用户补充信息后,模型在单次响应中,同时发起了对
get_current_weather和search_flights的调用(注意日志中出现了两次[正在查询信息...])。这是因为它判断出天气和航班是相对独立且可并行获取的信息。这展示了 Function Calling 支持并行工具调用的能力,可以提升效率。 - 信息整合与建议:在获得所有工具结果后,模型生成了一份结构清晰的汇总报告,并基于数据给出了初步建议(如对比航班时间、酒店特点)。这体现了 LLM 在信息整合与语言生成方面的核心价值。
- 持续交互:最后,Agent 将决定权交给用户,并邀请进一步反馈,形成了一个完整的服务闭环。
避坑指南:工具调用的“幻觉”与参数校验模型有时会产生“幻觉”,即调用一个不存在的工具,或生成不符合参数 Schema 的
arguments。例如,日期格式要求YYYY-MM-DD,模型可能输出“next Saturday”。必须在执行工具前进行严格的参数校验。我们的示例中直接json.loads和**func_args传递,在生产环境中是危险的。应该先验证参数类型、格式、枚举值,对缺失或错误的参数提供默认值或抛出清晰错误,并将错误信息通过tool角色消息返回给模型,让它有机会纠正。
5. 生产级考量与最佳实践
将一个小 demo 变成稳定可用的生产级 Agent,还需要跨越很多鸿沟。以下是几个关键点的深度解析。
5.1 工具设计的艺术:粒度、描述与错误处理
工具的设计质量直接决定 Agent 的能力上限。
- 工具粒度:是设计一个“万能”的
search工具,还是拆分成search_flights、search_hotels、search_attractions等多个工具?更细的粒度通常更好。细粒度工具功能单一,描述更精准,模型更容易理解和准确调用。一个“万能搜索”工具需要极其复杂的描述和参数,模型很难掌握。当然,也要避免过度拆分,导致工具数量爆炸。 - 描述即契约:工具的
description和参数的description是你与模型签订的“契约”。要用最清晰、无歧义的语言书写。好的描述示例:“查询从departure_city飞往arrival_city,在departure_date日期的直飞航班经济舱价格。” 差的描述:“搜索航班。” - 错误处理与鲁棒性:工具函数内部必须有完善的错误处理(网络超时、API 限流、无效输入等)。当错误发生时,不应直接崩溃,而应返回结构化的错误信息,例如
{"error": "API_TIMEOUT", "message": "航班查询服务暂时无响应,请稍后再试。"}。并将此错误信息通过tool角色返回给模型,模型可以据此向用户解释或尝试其他方案。
5.2 对话历史与上下文管理
OpenAI 的 GPT 模型有上下文长度限制(如 4K、8K、16K、128K tokens)。长对话中,历史消息会迅速耗尽额度。
- 选择性记忆:不是所有历史都需要原封不动地传递。可以设计策略,只保留最近 N 轮对话,或总结之前的长期记忆。
- 总结与压缩:一种高级技巧是让模型自身对过长的历史进行总结。例如,在历史达到一定长度后,插入一条系统消息:“请将上述对话总结成一个简洁的段落,保留用户的核心需求和已完成的行动要点。”然后将这个总结作为新的“压缩后的历史”开头,替换掉冗长的原始消息。
- 关键信息提取:对于 Agent 执行,用户的核心约束(如预算、日期、偏好)至关重要。可以主动将这些信息从历史中提取出来,作为系统提示词的一部分或单独维护,确保在长对话中不丢失。
5.3 超越基础 Function Calling:智能规划与工作流
我们之前的 Agent 是“反应式”的,根据当前对话状态决定下一步动作。更强大的 Agent 需要“前瞻性”规划。
- 任务分解(Task Decomposition):对于“帮我规划一个包含交通、住宿、景点和餐厅的七日欧洲游”这样的复杂请求,需要先将其分解为子任务。这可以通过让一个“规划器”LLM(或同一个 LLM 的特定提示)来完成,生成一个任务列表,然后由“执行器”LLM 按顺序或并行地调用工具完成每个子任务。
- ReAct + Function Calling 结合:你可以结合 ReAct 的链式推理和 Function Calling 的稳定性。让模型以“Thought: 我需要先确定目的地和日期。用户已提供... 接下来我应该调用天气工具了解当地气候。Action: 调用
get_current_weather...” 的方式思考,但“Action”部分输出的是标准的 Function Calling 结构。这样既有了可解释的推理链,又有了稳定的工具调用接口。 - 工作流引擎:对于极其复杂、有固定流程的业务(如客服工单处理、电商退货),可以引入外部的工作流引擎(如状态机)。LLM Agent 作为“决策节点”,根据当前状态和用户输入,决定工作流下一步走向哪个节点,每个节点关联特定的工具或操作。
5.4 成本、延迟与性能优化
频繁调用 LLM 和外部工具会产生成本和延迟。
- 缓存:对频繁且结果变化不大的查询(如城市信息、产品目录)进行缓存。可以在调用工具前先检查缓存。
- 批量处理:如果模型在一次响应中请求多个独立工具调用(如同时查天气和汇率),应尽可能并行执行这些工具调用,减少总体延迟。
- 模型选型:对于简单的工具调用决策,可以使用更小、更快的模型(如
gpt-3.5-turbo)。对于需要复杂规划、推理和总结的步骤,再使用gpt-4。这种混合使用策略可以优化成本和速度。 - 流式输出(Streaming):对于最终给用户的答案,如果生成时间较长,可以使用流式输出,让用户先看到部分内容,提升体验。
从理解 LLM 与 Agent 的根本区别,到掌握 Function Calling 这一核心桥梁技术,再到亲手构建从简单到复杂的 Agent,最后思考生产环境中的挑战与优化,这条路径清晰地勾勒出了 AI 应用开发从“玩具”到“工具”的演进方向。Agent 不是 LLM 的简单包装,而是为其装上了感知世界的传感器和改造世界的手脚。而 Function Calling,就是让大脑能精确控制手脚的神经协议。