1. 项目概述:从“问答机”到“执行者”的跨越
最近在折腾大模型应用开发的朋友,估计没少被“Agent”这个词刷屏。从去年开始,这个概念就火得不行,好像不提Agent,都不好意思说自己搞AI应用。但说实话,很多刚接触的朋友,包括我一开始也是,看着各种框架和教程,感觉Agent就是个“黑盒”——知道它能调用工具,但具体怎么让它听话、怎么设计它的思考逻辑,总有点雾里看水。
所以,今天我们不谈那些宏大的概念,就从一个最实际的问题出发:如何让一个本地部署的大模型,比如用Ollama跑的Llama 3,学会自己调用工具去解决一个具体问题?比如,我给它一个任务:“帮我查一下上海今天的天气,然后根据天气建议我是否要带伞,最后把这个建议用中文写成一个简洁的备忘录。” 如果只用基础的对话,模型可能会给你一段描述性的文字。但有了Agent,它能自己决定先调用一个天气查询工具获取数据,再根据数据推理,最后调用一个文本格式化工具输出结果。这个从“被动回答”到“主动规划并执行”的转变,就是Agent的核心魅力。
我选择用LangChain来实现这个想法。虽然现在Dify、Workbuddy这类低代码平台也很火,但LangChain提供了最大的灵活性和透明度,你能清楚地看到Agent的“思考链”(Chain of Thought),这对于学习和深度定制至关重要。很多人问LangChain工具调用和LLM本身的Function Call有什么区别?简单说,LLM的Function Call是模型原生能力,告诉模型有哪些函数可用,模型在回复中会声明它想调用哪个函数以及参数是什么,但执行还得靠开发者自己写代码去解析和执行。而LangChain的Agent,则是把工具定义、模型决策、工具执行、结果处理这一整套流程都封装好了,提供了一个更高阶的、可编排的自动化工作流。它负责驱动整个“思考-行动-观察”的循环。
那么,一个能自己调用工具解决问题的Agent,到底是怎么构建出来的?它的“大脑”(LLM)和“手脚”(Tools)如何协同?为什么有时候它表现得像个“人工智障”,乱调用工具或者陷入死循环?接下来,我就结合一次完整的实战,拆解LangChain Agent的构建过程、核心机制以及那些官方文档里不会写的“踩坑”经验。
2. 核心组件拆解:构建Agent的“五脏六腑”
在动手写代码之前,我们必须先理解组成一个LangChain Agent的核心部件。这就像组装一台电脑,你得清楚CPU、内存、硬盘各自的作用。
2.1 大脑:语言模型(LLM)
Agent的“大脑”就是大语言模型。它的核心职责是理解和规划:理解用户指令,规划解决问题的步骤序列(Plan),并在每一步决定是进行常规推理,还是调用某个工具(Action)。
模型选型考量:对于Agent任务,模型的选择至关重要。它需要具备较强的指令遵循(Instruction Following)和逻辑推理(Reasoning)能力。
- 云端API vs. 本地模型:为了完全离线、可控且无网络延迟,我选择了本地部署。Ollama是目前管理本地模型最方便的工具之一。在模型选择上,我测试了
Llama 3 8B、Qwen 7B和Hermes 2 Pro。最终选择了Llama 3 8B,因为它在工具调用和格式遵循上表现更稳定。Hermes 2 Pro虽然在某些对话任务上很出色,但在复杂的多步工具调用规划上,我实测发现它更容易“跑偏”或忘记之前的上下文。 - 关键参数配置:通过LangChain调用本地模型时,温度和
max_tokens是核心参数。- 温度(temperature):Agent任务需要确定性的决策,因此温度不宜过高。我通常设置为
0.1或0.2,以减少模型响应的随机性,让工具调用的决策更稳定。 - 最大令牌数(max_tokens):这限制了模型单次响应的长度。对于Agent,它的响应里包含了“思考过程”和“行动指令”,需要足够的空间。我一般设置为
512或1024。设置过低会导致响应被截断,Agent无法输出完整的决策。
- 温度(temperature):Agent任务需要确定性的决策,因此温度不宜过高。我通常设置为
注意:不要盲目追求大参数模型。对于大多数工具调用场景,一个7B-13B参数量的、精调过的模型(如专门为工具调用优化的版本),其表现往往优于一个未经优化的更大模型。推理速度也是一个重要因素。
2.2 手脚:工具(Tools)
工具是Agent与外部世界交互的“手脚”。一个工具本质上就是一个Python函数,加上清晰的描述。LangChain要求工具描述必须清晰,因为模型就是靠这个描述来决定是否以及何时调用它。
如何设计一个好工具?
- 单一职责:一个工具只做一件事。比如,
get_weather只负责查询天气并返回结构化数据,send_email只负责发送邮件。不要把查询天气和发送建议合并到一个工具里。 - 清晰的描述(description):这是最重要的部分。描述要像给一个“外星人”下指令一样精确。必须说明:这个工具是干什么的?输入参数是什么(名称、类型、含义)?输出是什么?例如:
模糊的描述会导致模型错误调用。“””根据城市名称查询该城市当前的天气情况。输入是一个字符串,代表城市名,如‘上海’。返回一个包含天气状况、温度、湿度等信息的JSON对象。“”” - 结构化的返回:尽可能返回结构化的数据(如字典、Pydantic模型),而不是一大段自然语言。这有利于模型在后续步骤中解析和使用这些数据。
2.3 协调中枢:Agent执行器(AgentExecutor)
这是LangChain Agent框架的“调度中心”。它负责驱动ReAct(Reasoning + Acting)循环:
- 将用户输入和当前状态(历史记录、中间结果)传递给LLM(大脑)。
- 解析LLM的响应。响应可能是:
Action: 模型决定调用某个工具,并给出工具名和输入参数。Final Answer: 模型认为已经得到最终答案,直接输出给用户。
- 如果解析到
Action,则查找对应的工具并执行,得到Observation(观察结果)。 - 将
Observation连同之前的记录,再次喂给LLM,让它进行下一轮“思考”。 - 重复此过程,直到模型输出
Final Answer或达到最大迭代次数。
AgentExecutor的关键配置:
max_iterations:必设参数!防止Agent陷入死循环。比如一个简单问题它可能调用十几次工具还没结果,这通常是模型“卡住”了。我一般设为5或6,对于复杂任务可以适当提高。handle_parsing_errors: 设置为True。当模型输出的格式不符合Agent预期的Action或Answer格式时(比如模型“说起了胡话”),这个处理器能捕获错误,并以一种友好的方式重新提示模型,而不是直接让程序崩溃。verbose: 开发时设为True,这样可以在控制台看到完整的ReAct循环日志,包括模型的“思考”和每一步的“行动”、“观察”,对于调试无比重要。
2.4 思维框架:Agent类型与提示模板
LangChain提供了多种预定义的Agent类型,如ZERO_SHOT_REACT_DESCRIPTION,CONVERSATIONAL_REACT_DESCRIPTION等。它们本质上是不同的提示模板(Prompt Template),预先写好了引导模型进行ReAct推理的指令。
ZERO_SHOT_REACT_DESCRIPTION: 最常用的一种。它不保留对话历史(每次都是新会话),提示词明确要求模型以“Thought:”, “Action:”, “Observation:”的格式进行响应。它适合一次性的、任务型的场景。CONVERSATIONAL_REACT_DESCRIPTION: 适用于多轮对话场景,它会将历史对话也纳入上下文,让Agent能基于之前的交流进行决策。
提示模板的魔力:你可以完全自定义这个模板。官方模板是一个很好的起点,但如果你发现模型总是忽略某个工具,或者格式经常出错,可以微调提示词。例如,在工具列表前加上强调:“你必须从以下工具中选择一个来使用,不能自己编造工具。” 这能显著提升工具调用的准确性。
3. 实战:构建一个本地天气助手Agent
理论说得再多,不如一行代码。我们现在就来构建一个能完成开篇那个任务的Agent:查询天气并生成建议备忘录。
3.1 环境准备与模型本地化部署
首先,确保你的环境已经就绪。我使用Python 3.10+的环境。
# 安装核心库 pip install langchain langchain-community langchain-core # 安装Ollama的LangChain集成包(如果你用Ollama) pip install ollama # 或者,如果你使用其他本地API服务器(如LM Studio、text-generation-webui提供的兼容OpenAI的API) pip install openai部署本地模型:我使用Ollama,因为它最简单。
# 拉取Llama 3 8B模型(确保你的磁盘空间和内存足够) ollama pull llama3:8b # 运行模型服务,Ollama默认会在本地11434端口启动API服务 ollama run llama3:8b此时,一个兼容OpenAI API格式的本地服务就已经在http://localhost:11434运行了。
3.2 定义工具:给Agent装上“手”和“眼”
我们将定义两个工具:一个天气查询工具,一个文本格式化工具。
import requests from langchain.tools import tool from datetime import datetime # 工具1:模拟天气查询工具 # 在实际项目中,这里应该调用真实的天气API,如和风天气、OpenWeatherMap等。 # 为了演示和离线环境,我们模拟一个。 @tool def get_weather(city: str) -> str: """根据城市名称查询该城市当前的天气情况。输入是一个字符串,代表城市名,如‘上海’。返回一个格式化的天气信息字符串。""" # 模拟数据 - 真实情况请替换为API调用 weather_data = { "上海": {"condition": "多云", "temp": 22, "humidity": 65, "rain_probability": 30}, "北京": {"condition": "晴", "temp": 18, "humidity": 40, "rain_probability": 10}, "广州": {"condition": "雷阵雨", "temp": 28, "humidity": 85, "rain_probability": 80}, } if city in weather_data: data = weather_data[city] result = f"{city}的天气:{data['condition']},温度{data['temp']}°C,湿度{data['humidity']}%,降雨概率{data['rain_probability']}%。" return result else: return f"未找到{city}的天气信息。" # 工具2:文本格式化工具(模拟生成备忘录) @tool def format_memo(content: str, style: str = "简洁") -> str: """将给定的内容按照指定风格格式化成备忘录。输入是内容字符串和风格字符串(默认为‘简洁’)。返回格式化后的文本。""" current_time = datetime.now().strftime("%Y-%m-%d %H:%M") if style == "简洁": memo = f"【备忘录】{current_time}\n\n{content}\n\n--- 结束 ---" elif style == "正式": memo = f"致:相关人员\n发件人:AI助手\n日期:{current_time}\n主题:天气建议\n\n{content}\n\n此致\n敬礼" else: memo = f"内容:{content}" return memo # 将工具放入列表,供Agent使用 tools = [get_weather, format_memo]关键点:@tool装饰器是LangChain社区版(langchain-community)中定义工具的标准方式。它自动将函数及其文档字符串转换为LangChain能识别的工具对象。确保你的函数有类型注解和清晰的docstring,这能极大帮助模型理解。
3.3 连接大脑:初始化本地LLM
接下来,我们使用LangChain连接到本地运行的Ollama服务。
from langchain.llms import Ollama # 或者使用ChatOllama,它对消息格式处理更好 from langchain.chat_models import ChatOllama from langchain.schema import SystemMessage, HumanMessage # 方式一:使用基础的Ollama类(较简单) # llm = Ollama(base_url="http://localhost:11434", model="llama3:8b", temperature=0.1) # 方式二:使用ChatOllama(推荐,更符合对话/Agent场景) llm = ChatOllama( base_url="http://localhost:11434", model="llama3:8b", temperature=0.1, # 可以传入系统提示词来进一步约束模型行为 # system="你是一个乐于助人且严谨的AI助手。在回答时,请严格遵循ReAct格式进行思考。" )为什么用ChatOllama?因为大多数现代LLM(包括Llama 3)都是基于“消息”(System, User, Assistant)格式训练的。ChatOllama能更好地处理这种格式,在与Agent配合时,生成符合Action/Final Answer格式的响应更可靠。
3.4 组装Agent并运行
现在,把大脑(LLM)、手脚(Tools)和调度器(AgentExecutor)组装起来。
from langchain.agents import create_react_agent, AgentExecutor from langchain import hub # 1. 获取ReAct提示模板 # LangChain有一个“提示词中心”,可以拉取预定义的优质提示模板 prompt = hub.pull("hwchase17/react") # 你可以打印出来看看 prompt.template 里面是什么,理解其结构 # print(prompt.template) # 2. 创建Agent agent = create_react_agent(llm=llm, tools=tools, prompt=prompt) # 3. 创建执行器 agent_executor = AgentExecutor( agent=agent, tools=tools, handle_parsing_errors=True, # 关键!处理解析错误 max_iterations=6, # 防止无限循环 verbose=True, # 开发时打开,看详细思考过程 ) # 4. 运行Agent! question = "帮我查一下上海今天的天气,然后根据天气建议我是否要带伞,最后把这个建议用中文写成一个简洁的备忘录。" try: result = agent_executor.invoke({"input": question}) print("\n" + "="*50) print("最终答案:") print(result["output"]) except Exception as e: print(f"运行出错:{e}")当你运行这段代码,并将verbose=True时,控制台会输出类似下面的精彩过程:
> Entering new AgentExecutor chain... Thought: 用户想了解上海的天气,并根据天气决定是否带伞,最后生成备忘录。我需要先获取天气信息。 Action: get_weather Action Input: {"city": "上海"} Observation: 上海的天气:多云,温度22°C,湿度65%,降雨概率30%。 Thought: 降雨概率是30%,不算很高,但多云天气有可能下雨。为了保险起见,建议带伞。现在我需要将这个建议格式化成简洁的备忘录。 Action: format_memo Action Input: {"content": "根据天气预报,上海今天多云,降雨概率30%。建议携带雨伞以备不时之需。", "style": "简洁"} Observation: 【备忘录】2024-05-27 14:30\n\n根据天气预报,上海今天多云,降雨概率30%。建议携带雨伞以备不时之需。\n\n--- 结束 --- Thought: 我已经完成了所有步骤:查询了天气、给出了带伞建议、并生成了备忘录。现在可以给出最终答案了。 Final Answer: 已完成您的请求。查询到上海今日天气为多云,降雨概率30%,建议携带雨伞。已生成简洁备忘录如下:\n【备忘录】2024-05-27 14:30\n\n根据天气预报,上海今天多云,降雨概率30%。建议携带雨伞以备不时之需。\n\n--- 结束 --- > Finished chain.看到这个输出,你会清晰地看到模型的“思考”(Thought)、“行动”(Action)和从工具获得的“观察”(Observation)。这就是ReAct框架的可解释性,也是调试Agent的黄金标准。
4. 性能调优与深度解析
让Agent跑起来只是第一步,让它跑得“快、准、稳”才是挑战。下面分享几个关键的调优点和深度解析。
4.1 工具调用速度瓶颈分析
很多人抱怨LangChain Agent慢,尤其是调用本地模型时。速度瓶颈主要来自以下几方面:
- 模型推理速度:这是最大的瓶颈。本地7B/8B模型在普通CPU上推理,一次生成可能需要数秒甚至十几秒。解决方案:使用GPU加速(CUDA),或者选择更小、更快的模型(如Phi-3 mini)。对于生产环境,考虑使用量化后的模型(GGUF格式,用
llama.cpp或text-generation-webui加载),推理速度会有数量级提升。 - 网络延迟(如果使用云端API):每个工具调用前后的模型推理都需要一次网络往返。解决方案:优化提示词,减少不必要的思考步骤;使用流式响应(如果支持)来提升感知速度;考虑在离用户更近的区域部署模型或API网关。
- 工具本身执行时间:如果你的工具需要调用一个慢速的外部API(比如某些需要复杂计算的API),那么整个Agent会被阻塞。解决方案:对工具进行超时设置和错误重试;考虑将同步工具改为异步执行(LangChain支持异步Agent)。
- ReAct循环次数:每次循环都是一次完整的模型调用。不必要的循环会累积延迟。优化方法:通过设计更精准的工具描述和提示词,引导模型用更少的步骤解决问题。例如,如果任务明确是“查天气”,可以在提示词中暗示“你可以直接使用get_weather工具”。
4.2 提示工程:让Agent更“听话”
Agent的“智商”很大程度上取决于你给它的提示词。hwchase17/react这个默认模板已经不错,但我们可以针对特定场景优化。
自定义提示模板示例:
from langchain.prompts import PromptTemplate custom_prompt = PromptTemplate.from_template( """你是一个专业的任务处理助手。请严格按以下步骤思考: 工具列表: {tools} 使用工具时,请严格使用以下JSON格式: {{ "action": "工具名", "action_input": "工具的输入参数" }} 请开始!记住,你必须使用上述工具之一,不能编造工具。 用户问题:{input} 你之前的步骤(如果有):{agent_scratchpad} 请开始你的思考:""" ) # 然后用这个 custom_prompt 替换 hub.pull 的提示 agent = create_react_agent(llm=llm, tools=tools, prompt=custom_prompt)优化技巧:
- 强调格式:在提示词中明确写出模型应该输出的JSON格式示例,可以大幅减少解析错误。
- 限制行动空间:明确告知“你必须使用上述工具之一”,防止模型尝试回答“我不知道怎么用工具,但我认为...”。
- 提供示例(Few-Shot):对于复杂任务,可以在提示词中加入一两个完整的ReAct示例,教模型如何推理。这被称为“少样本提示”,效果极佳。
4.3 错误处理与鲁棒性提升
一个健壮的Agent必须能处理各种意外。
工具调用失败:网络错误、API限流、参数错误等。需要在工具函数内部做好
try...catch,并返回一个清晰的错误信息作为Observation,让模型知道发生了什么并可能调整策略。@tool def get_weather(city: str) -> str: try: # ... API调用 ... return result except requests.exceptions.RequestException as e: return f"错误:无法连接到天气服务。原因:{str(e)}。请检查网络或稍后再试。" except KeyError: return f"错误:未找到城市‘{city}’的天气数据。请确认城市名称是否正确。"模型输出格式错误(Parsing Error):这是最常见的问题。模型可能不按
Action:格式输出,而是说了一堆废话。这就是为什么AgentExecutor的handle_parsing_errors=True如此重要。当设置后,LangChain会捕获解析错误,并向模型发送一个类似“你的格式不对,请重试”的提示,让它重新生成。你还可以自定义这个错误处理函数。无限循环:模型可能在一个步骤里来回调用相同的工具,或者陷入“思考-不行动”的循环。除了设置
max_iterations硬性限制外,可以在agent_scratchpad(记录了之前所有步骤的变量)中检测重复动作,并在提示词中加入警告,如“注意,你已经查询过这个城市的天气了,请基于已有信息进行下一步。”
5. 进阶探索:从单Agent到多Agent与工作流
当单个Agent无法处理复杂任务时,我们就需要更高级的模式。
5.1 LangChain Agent vs. LangGraph
这是最近常被问到的问题。简单来说:
- LangChain Agent:专注于单个代理的“思考-行动”循环。它结构相对简单,适合线性任务。
- LangGraph:是LangChain的一个扩展库,用于构建有状态、多参与者的图工作流。你可以把多个Agent(或任何函数)作为节点,用条件逻辑(边)连接它们,构建复杂的、分支的、循环的工作流。
适用场景对比:
- 你的任务是一个清晰的、线性的“用户提问 -> Agent规划并执行工具 -> 返回答案”流程?用LangChain Agent就够了。
- 你的任务需要多个专家Agent协作(比如一个负责检索,一个负责分析,一个负责生成报告),或者流程中存在“如果...那么...”的分支判断,或者需要维护一个跨步骤的共享状态?那么LangGraph是更好的选择。
例如,用LangGraph可以轻松构建一个“评审-修改”循环:Writer Agent生成初稿,Critic Agent评审并提出修改意见,然后流程跳回Writer Agent进行修改,直到Critic Agent满意为止。这种带循环和状态的工作流,用基础的Agent实现起来会很别扭。
5.2 构建一个简单的多Agent系统雏形
即使不使用LangGraph,我们也可以用多个AgentExecutor组合起来。思路是:创建一个“主管Agent”,它的工具是调用其他“专家Agent”。
# 假设我们已经定义了一个天气查询Agent:weather_agent_executor # 和一个备忘录生成Agent:memo_agent_executor @tool def delegate_to_weather_agent(query: str) -> str: """将关于天气查询的复杂问题委托给专门的天气助手处理。输入是用户关于天气的原始问题。""" # 这里可以做一些输入预处理 result = weather_agent_executor.invoke({"input": query}) return result["output"] @tool def delegate_to_memo_agent(content: str) -> str: """将格式化文本和生成备忘录的任务委托给专门的文案助手处理。""" result = memo_agent_executor.invoke({"input": f"请将以下内容格式化为简洁备忘录:{content}"}) return result["output"] # 主管Agent的工具列表 supervisor_tools = [delegate_to_weather_agent, delegate_to_memo_agent, ...] # 为主管创建Agent和Executor supervisor_prompt = hub.pull("hwchase17/react") supervisor_agent = create_react_agent(llm=llm, tools=supervisor_tools, prompt=supervisor_prompt) supervisor_executor = AgentExecutor(agent=supervisor_agent, tools=supervisor_tools, verbose=True) # 用户向主管提问 final_result = supervisor_executor.invoke({"input": "上海和北京天气怎么样?对比一下,然后给我个出差建议。"})这种方式实现了简单的任务分发。但对于更复杂的协作和流程控制,学习LangGraph将是更高效的选择。
6. 常见问题与排查实录
在开发Agent的过程中,我踩过不少坑。这里列几个典型问题及其解决方法。
问题1:Agent总是输出“Final Answer: I don't know”或者不调用工具。
- 可能原因1:工具描述不清。模型不理解你的工具能干什么。解决:重写工具的描述(
docstring),确保清晰、无歧义,并包含输入输出示例。 - 可能原因2:提示词不适合。默认的ReAct提示词可能对你的模型效果不好。解决:尝试拉取不同的提示模板(如
hwchase17/react-chat用于对话),或者进行上文提到的自定义提示工程。 - 可能原因3:模型能力不足。某些小模型或未针对工具调用优化的模型,工具调用能力很弱。解决:换用更强的模型,或者寻找针对工具调用进行过微调的模型版本(如
ToolLlama、ChatGLM3的Tool Calling版本)。
问题2:Agent陷入死循环,反复调用同一个工具。
- 可能原因1:工具返回的信息不足以让模型做出决策。比如,查询天气返回“天气很好”,但模型需要具体的“降雨概率”来判断是否带伞,于是它反复查询,希望得到更详细的数据。解决:优化工具返回的信息,确保其包含决策所需的关键数据字段。
- 可能原因2:模型上下文混乱。在长循环中,之前的
Thought、Action、Observation都堆在上下文里,可能导致模型困惑。解决:尝试使用CONVERSATIONAL类型的Agent,它可能对长上下文管理更好。或者,在自定义提示词中,明确要求模型总结当前状态,避免重复劳动。 - 终极方案:必须设置
max_iterations!这是安全绳。
问题3:解析错误(Parsing Error)频繁发生。
- 可能原因1:模型输出格式不稳定。解决:开启
handle_parsing_errors=True。此外,可以尝试降低temperature到0,增加输出的确定性。在提示词中强化格式要求(用JSON示例)。 - 可能原因2:使用了不适合的Chat模型/非Chat模型。有些纯补全模型(Completion Model)不擅长输出结构化的
Action格式。解决:确保使用Chat模型(如ChatOllama,ChatOpenAI)。
问题4:本地模型调用速度太慢,无法忍受。
- 可能原因:模型未量化,在CPU上推理。解决:
- 使用GPU:确保你的PyTorch或相关库安装了CUDA版本,并且模型加载到了GPU上。
- 模型量化:将模型转换为GGUF格式,并使用
llama.cpp、gpt4all或text-generation-webui进行加载和推理。4-bit或5-bit量化能在几乎不损失精度的情况下,大幅提升推理速度并降低内存占用。Ollama本身也使用了量化技术。 - 使用更小的模型:对于许多工具调用任务,3B-7B量级的精调模型可能已经足够,速度会快很多。
问题5:如何让Agent使用自定义的Python函数/类方法?
- 解决:
@tool装饰器是首选。它可以直接装饰你的函数。对于类方法,你需要先定义一个函数来包装这个方法,或者使用Tool.from_function()方法。
更推荐的方式是使用Pydantic来定义严格的输入模式,这能让模型更准确地生成参数。from langchain.tools import Tool class MyCalculator: def add(self, a: int, b: int) -> int: return a + b calc = MyCalculator() # 将类方法包装成工具 add_tool = Tool( name="Calculator_Add", func=lambda x: calc.add(**x), # 注意输入可能是字典 description="将两个整数相加。输入是一个包含'a'和'b'两个键的字典,如{{'a': 5, 'b': 3}}。返回它们的和。", args_schema=... # 可以定义更严格的输入模式 )
构建一个稳定可靠的Agent,是一个不断迭代和调试的过程。从最简单的工具和提示词开始,逐步增加复杂性,并始终通过verbose=True来观察其内部思考过程,是最高效的学习和开发方式。当你看到大模型按照你设计的逻辑,一步步调用工具,最终解决一个实际问题时,那种成就感是无可替代的。这不仅仅是调用API,而是在塑造一个能够自主行动的智能体。