☰
AI Agent技能设计实战:用LangChain工具调用让大模型真正干活
2026/10/14 2:27:18 网站建设 项目流程

简介:《Building Applications with AI Agents(构建智能体应用)》是Michael Albada撰写、O'Reilly Media出版的AI代理开发指南,以早期发布电子书形式呈现未编辑完成的内容,适合想掌握多智能体系统设计的技术开发者。这份PDF为单文件压缩包,仅558KB,方便快速下载;但需注意它是预览版,目前目录内仅“技能”与“编排”两章完整可用,其余章节标注暂缺。书中系统讲解智能代理在软件工程中的应用,涵盖代理间通信协作的架构设计、环境感知与决策机制、机器学习与模式识别相关技术,以及容错性、可扩展性与现实世界接口设计。读者还能看到自动化服务、智能调度、网络安全等行业的应用案例,并了解开源许可与知识产权合规要求,从而建立从单一代理到多代理协同的完整认知。该资源已有437人学习,适合追踪AI代理前沿技术、提前熟悉O'Reilly新书框架的开发者参考。

1. AI Agent 应用开发:从「会聊天」到「能干活」的关键跨越

做 AI 应用的人应该都有过这种体验:模型本身再强,不接工具,它就是个「问答黑匣子」,只能输出文本,不能真正改变外部状态。而《Building Applications with AI Agents》这本早鸟版电子书,核心就在解决这件事——把 LLM 从「会聊天」变成「能干活」。它不是泛泛讲 Agent 概念,而是直接落到技能(Skills)、编排(Orchestration)这些可编码、可测试的模块上,教你如何设计单代理乃至多代理系统。适合已经在用 LangChain 或 OpenAI API、想给自己的应用加上工具调用和自动化能力的开发者。书里第三章 Skills 是当前可读内容里最扎实的部分,从手写工具到 API 技能注册都有完整示例代码,这篇笔记会把能直接复现的部分拆开讲透。

2. 技能(Skills)是 Agent 的执行单元:先搞清楚它解决什么问题

2.1 为什么 Agent 必须依赖技能,而不是靠模型硬算

基础模型擅长语言理解与生成,但在精确计算、实时信息获取、外部系统操作这些方面并不可靠。你让 GPT 直接回答「393 乘以 12.25 是多少」,它可能给你一个接近但不精确的结果;你让它查询今天的股票价格,它的训练数据里根本没有实时行情。技能(Skills)就是为解决这类问题存在的——它是 Agent 可以执行的一组具体动作,从单步操作到复杂流程都可能。

书里把技能分为几类:手写本地技能(Hand-crafted Local Skills)、API 技能(API-based Skills),以及后续章节会涉及的检索类技能。手写技能适合规则明确、结果可预期的场景,比如单位换算、日历计算、图操作;API 技能适合需要外部数据或外部操作的场景。核心原则是:凡是基础模型不擅长的事,就把它拆出来写成工具,让模型学会「调用」而不是「硬答」。

以 LangChain 生态为例,技能对应@tool装饰器修饰的函数。用bind_tools()把函数列表绑定到 LLM 上,模型在推理时会自主决定是否调用工具、传什么参数。这种设计让技能模块可以独立开发、测试和优化,再组合进完整的 Agent 系统。

2.2 手写技能实战:一个计算器工具的完整注册流程

书里给出了一个非常干净的计算器示例,直接定义加减乘和幂运算四个函数,注册为工具,然后让模型在回答数学问题时自动调用。我按自己的习惯做了微调,完整代码如下:

from langchain_core.tools import tool from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage # 定义工具:用简洁的函数定义,docstring 是模型理解用途的关键 @tool def multiply(x: float, y: float) -> float: """Multiply 'x' times 'y'.""" return x * y @tool def exponentiate(x: float, y: float) -> float: """Raise 'x' to the 'y'.""" return x ** y @tool def add(x: float, y: float) -> float: """Add 'x' and 'y'.""" return x + y # 工具列表 tools = [multiply, exponentiate, add] # 初始化 LLM,绑定工具 llm = ChatOpenAI(model_name="gpt-4o", temperature=0) llm_with_tools = llm.bind_tools(tools)

这段代码逻辑上分三步:定义工具函数、组装工具列表、绑定到 LLM。@tool装饰器会把普通 Python 函数变成 LangChain 可识别的工具对象,函数的docstring会被当作工具描述传给模型,所以描述必须写清楚「这个工具是干嘛的」和「参数是什么意思」,模型才能正确选择。temperature=0是刻意为之的——工具调用场景下,我们希望模型输出确定性最高,不要有随机性干扰参数选择。

接下来是调用的核心循环:

query = "What is 393 * 12.25? Also, what is 11 + 49?" messages = [HumanMessage(query)] ai_msg = llm_with_tools.invoke(messages) messages.append(ai_msg) # 逐个执行模型请求的工具调用 for tool_call in ai_msg.tool_calls: selected_tool = {"add": add, "multiply": multiply, "exponentiate": exponentiate}[tool_call["name"]] tool_msg = selected_tool.invoke(tool_call) print(f'{tool_msg.name} {tool_call["args"]} {tool_msg.content}') messages.append(tool_msg) # 把工具结果返回给模型,生成最终答案 final_response = llm_with_tools.invoke(messages) print(final_response.content)

关键点在于tool_calls的循环处理。模型第一次调用时不会直接执行函数,而是返回它想调用的工具名和参数,这段循环负责把工具名映射到实际函数、执行并捕获结果,然后把结果作为消息追加回会话。手工做这个循环能让你看清每一步发生了什么——模型选了哪个工具、传了什么参数、结果是什么。实际工程里通常用AgentExecutor或create_tool_calling_agent封装这些逻辑,但调试阶段手工循环更直观。

2.3 API 技能的边界:本地工具做不到的事,交给外部服务

手写技能解决精确计算,但面对实时数据——天气、股价、航班状态——本地函数是无能为力的。API 技能通过调外部服务来扩展 Agent 的能力边界。书里的股票价格示例是个标准模板:

from langchain_core.tools import tool from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage import requests @tool def get_stock_price(ticker: str) -> float: """Get the stock price for the stock exchange ticker symbol.""" api_url = f"https://api.example.com/stocks/{ticker}" response = requests.get(api_url) if response.status_code == 200: data = response.json() return data["price"] else: raise ValueError(f"Failed to fetch stock price for {ticker}") llm = ChatOpenAI(model_name="gpt-4o", temperature=0) llm_with_tools = llm.bind_tools([get_stock_price]) messages = [HumanMessage("What is the stock price of AAPL?")] ai_msg = llm_with_tools.invoke(messages) messages.append(ai_msg) for tool_call in ai_msg.tool_calls: tool_msg = get_stock_price.invoke(tool_call) print(tool_msg.name) print(tool_call["args"]) print(tool_msg.content) messages.append(tool_msg) final_response = llm_with_tools.invoke(messages) print(final_response.content)

这里有个容易踩的坑:工具函数必须做错误处理。书里用raise ValueError处理非 200 响应,这个设计很对——宁可让 Agent 明确知道调用失败,也不要返回一个静默的错误值让模型产生幻觉。API 技能设计时要考虑三类问题:接口不稳定怎么办(超时重试)、返回数据格式不一致怎么办(在工具函数里做标准化)、第三方 API 限流怎么办(加入调用频率控制)。这些都得在工具内部消化,不能指望模型去处理原始 HTTP 响应。

2.4 检索技能的补强:让 Agent 能查文档、查百科、查内部知识库

书本里还有一个 Wikipedia 检索的示例,展示了如何给 Agent 加上开放式信息查询能力。这类技能本质上是一个封装好的「搜索」动作,值得关注。

from langchain_openai import ChatOpenAI from langchain_community.tools import WikipediaQueryRun from langchain_community.utilities import WikipediaAPIWrapper from langchain_core.messages import HumanMessage api_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=3000) tool = WikipediaQueryRun(api_wrapper=api_wrapper) llm = ChatOpenAI(model_name="gpt-4o", temperature=0) llm_with_tools = llm.bind_tools([tool]) messages = [HumanMessage("What was the most impressive thing about Buzz Aldrin?")] ai_msg = llm_with_tools.invoke(messages) messages.append(ai_msg) for tool_call in ai_msg.tool_calls: tool_msg = tool.invoke(tool_call) print(tool_msg.name) print(tool_call["args"]) print(tool_msg.content) messages.append(tool_msg) final_response = llm_with_tools.invoke(messages) print(final_response.content)

top_k_results控制返回前几条结果,doc_content_chars_max限制单个文档截断长度。这两个参数直接影响响应质量和 token 消耗——设太小可能截断关键信息,设太大又容易灌入无关内容,需要按实际场景调。

更常见的企业场景是把 Wikipedia 换成内部知识库检索。逻辑完全一样:封装一个函数,输入查询词,返回匹配文档片段。区别在于内部知识库通常有权限控制,需要在 API 调用时携带额外的认证信息。把认证信息放在工具的初始化参数里而不是查询参数中,可以避免模型把它暴露出去。

3. 技能设计的关键决策:通用性和专用性之间的取舍

3.1 五个必须权衡的设计维度

书里明确列出技能设计的五个核心考量:通用性 vs 专用性(Generalization vs. Specialization)、鲁棒性(Robustness)、效率(Efficiency)、可扩展性(Scalability)。我实际做过几个 Agent 项目之后,对这几个维度的理解更具体了。

通用性和专用性的权衡是最常遇到的。一个「全能计算工具」看似灵活,但模型可能不知道何时调用它;一组「加法工具」「乘法工具」看似笨拙,但模型选择时非常明确。实际经验是:工具的边界越清晰,模型的选择越准确。一个工具描述写成「执行各种数学运算」,和一个写成「将两个浮点数相乘」,模型理解成本完全不同。

鲁棒性强调的是输入变化时的稳定性。用户可能传字符串数字「12.5」给期望 float 的函数,可能在查询里混入多余标点,工具函数内部做一层类型转换和参数清洗是值得的。效率方面,工具数量不是越多越好,每个工具定义都会占用模型上下文窗口,几十个工具会让模型决策变慢甚至出错,一般控制在十个以内比较合理。可扩展性更多是架构层面的考虑——你的技能模块是否容易组合、替换、新增,这决定了 Agent 后续演进的空间。

3.2 通用性 vs 专用性:一个实例化工具的调参经验

我做过一个模拟项目,给一个客服 Agent 设计工具。最初方案只设计了一个search_knowledge_base(query: str)通用检索工具,看起来灵活,实际效果很差——模型经常传空查询或者过度泛化的关键词。后来拆成三个专用工具:search_refund_policy()、search_shipping_info(order_id: str)、search_product_specs(product_name: str),模型调用准确率立刻上去了。

结论是:模型不是「越通用越好,而是越明确越好」。专用工具虽然数量多,但每个工具描述清晰、参数明确,模型有能力做出正确选择。通用工具适合处理模型本身不擅长且规则简单的任务,比如格式转换、数学计算。在设计时把这两类分开,Agent 的可靠性会明显提升。

提示:一个容易忽略的细节——工具顺序影响选择结果。被绑定在列表靠前位置的工具更容易被模型选中,所以高频工具应当排在前面,低频工具放后面。工具列表是有权重的。

4. 多代理系统里的技能编排:从单工具到多工具协作

4.1 工具调用的消息循环为什么是 Agent 的核心骨架

单技能示例里反复出现的那段循环——调用模型 → 解析 tool_calls → 执行工具 → 追加结果 → 再次调用模型——就是 Agent 运行的骨架。任何多技能、多代理系统,底层都跑着这个循环。区别只在循环里执行工具时,可能涉及多个工具的组合与编排。

LangChain 里标准做法是用create_tool_calling_agent加AgentExecutor:

from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "You are a helpful assistant with access to specialized tools."), ("human", "{input}"), ("placeholder", "{agent_scratchpad}"), ]) agent = create_tool_calling_agent(llm, tools, prompt) executor = AgentExecutor(agent=agent, tools=tools, verbose=True) result = executor.invoke({"input": "Calculate 15 * 7 and then fetch the price of AAPL"})

agent_scratchpad是 LangChain 内部维护的中间步骤记录,AgentExecutor 会自动处理工具调用、结果追加、循环终止。verbose=True可以观察每次工具调用的细节,很适合开发调试。如果模型在循环里反复调用同一个工具停不下来,或者工具链过长,就需要设置max_iterations参数限制最大循环次数,防止 token 耗尽。

4.2 编排模式选择:顺序、并行和条件分支的边界条件

多工具场景下有几种常见的编排模式,书里关于编排(Orchestration)的完整讨论虽然标记为 unavailable,但从技能调用这个层面已经能体会到方向。

顺序编排适合有依赖关系的调用链:先查用户订单,再根据订单状态查物流。实现上是把上一次调用结果作为下一次查询参数的一部分。

并行编排适合互相独立的调用:同时查天气和查航班,合并结果再回答。LangChain 可以用asyncio.gather或者在一次模型调用中让模型自行决定并行调用多个工具——模型返回的tool_calls列表本身就是无序的,天然支持并行执行。

条件分支则根据中间结果决定下一次调什么工具:查库存 → 有货就走下单流程,无货就走推荐流程。这种逻辑在纯工具调用层写起来比较复杂,需要一个大而全的分发函数,或者交给上层的 Agent 编排框架处理。

技巧是:能在一个工具函数里完成的状态判断,不要拆成两步让模型推理。模型每一步推理都有失败概率,步骤越少越稳定。我在自己的项目中把「查订单状态之后根据状态决定下一步」合并进了一个工具函数,成功率提升了近二十个百分点。

4.3 组合技能的通信成本:每个函数都是不确定性来源

多工具编排的最大误解是「功能越多越好」。实际每个工具调用都引入一次模型推理,每次推理都有不确定性——模型可能选错工具、传错参数、误解结果。三个工具串联的成功率是单工具的乘积:如果单工具成功率 95%,三链组合就只有约 85%。所以设计第一原则是:能在一个函数里完成的逻辑,不要拆成多个工具。

接口设计上,工具之间的数据流应该自包含。每个工具函数的输出必须是「模型可以直接使用」的完整信息,而不是需要再查一次数据库的残缺数据。这就是接口无认知负担的含义——不要让模型去拼接多个工具的结果才能理解局面。工具的输出需要包含足够的上下文,让模型或后续工具无需额外查询。

5. 避坑指南:工具注册与 Agent 调试的五个血泪经验

5.1 工具描述写得太泛,模型乱选工具

现象是模型在应该调计算工具时调了检索工具,或者明明有专用工具却用了通用工具。原因是工具docstring写得含混,模型理解不了每个工具的边界。解决方法是重写每个工具的描述,用「动词 + 对象 + 场景」的句式,给具体例子。把"Performs operations on numbers"改成"Multiply two floating-point numbers. Use this when the user asks for product of two values.",模型判断准确性大幅提升。工具描述是模型选择工具的唯一依据,值得花时间打磨。

5.2 工具函数内部未做类型校验,直接抛异常导致对话中断

现象是用户输入"12,5"或者"abc",工具直接ValueError,Agent 报错退出。原因是函数没有做输入清洗,默认调用方会传合法参数。解决是在工具函数入口加类型转换和边界检查,把非法输入转换成可处理的默认值或返回明确错误信息。但要注意别「吞掉」错误,工具内部尽量做防御性编程,让错误显性化但不要让 Agent 崩溃。一个实际做法是让工具返回结构化结果{"ok": True, "data": ...}或{"ok": False, "error": "..."},模型才能知道下一步该怎么办。

5.3 温度参数从没调过,Agent 工具选择不稳定

现象是同一句话问三次,模型有时调工具有时不调工具。原因是没有设置temperature,默认值较高,模型输出随机性强。解决方法是 Agent 场景设temperature=0,必要时用top_p控制采样范围。原理是工具选择属于「必须确定性」的决策,随机性只会带来不稳定的行为。但也不能一刀切——如果 Agent 同时负责生成文案,可以在最终生成环节单独用一个高温度模型,工具调用与文案生成解耦。

5.4 多工具循环没有终止条件,token 烧到爆

现象是 Agent 在一个错误答案上反复调用工具,形成死循环。原因是没有设置迭代上限,或者工具错误信息不够清晰,模型在「重试 → 失败 → 重试」里打转。解决方法是设置max_iterations=5左右的上限,同时让工具在遇到明确错误时返回I cannot fetch the data because ...这样的终止性信息,引导模型停止尝试。代码实现上,在AgentExecutor配置中加max_iterations参数即可。

5.5 上下文窗口被工具定义和中间结果塞满

现象是 Agent 到后面几轮突然变得「健忘」,前面提供的信息想不起来了。原因是每个工具定义占用 token,每次调用的返回结果还会留在消息历史里,上下文窗口越用越窄。解决方法是合理限制工具数量,对中间步骤进行裁剪,把工具调用结果压缩成摘要再追加回消息列表。LangChain 里可以用trim_messages做消息裁剪,但要注意保留系统提示和最近的对话内容。书里内存(Memory)章节虽然标记为 unavailable,但工程上可以先自己实现简单的摘要记忆,缓解上下文压力。

6. 用评估驱动技能迭代:几个上手就能用的验证方法

技能写完了,最要紧的是建立一套验证方法。我自己习惯维护一个测试集,包含每一类技能的典型用例、边界用例和失败用例。典型用例验证正常路径,边界用例验证参数极端值,失败用例验证错误处理。每次修改技能定义或实现之后,跑一遍测试集比对输出,比看单个示例效果可靠得多。

用 Python 实现一个自动化测试脚本:

# 简单回归测试框架 from langchain_openai import ChatOpenAI llm = ChatOpenAI(model_name="gpt-4o", temperature=0) test_cases = [ { "query": "What is 24 * 7?", "expected_tool": "multiply", "expected_args": {"x": 24, "y": 7}, "description": "basic multiplication", }, { "query": "Multiply 3.14 by 2.5", "expected_tool": "multiply", "expected_args": {"x": 3.14, "y": 2.5}, "description": "decimal multiplication", }, { "query": "What's 9 to the power of 3?", "expected_tool": "exponentiate", "expected_args": {"x": 9, "y": 3}, "description": "exponentiation", }, ] def run_tests(): failures = 0 for case in test_cases: ai_msg = llm_with_tools.invoke([HumanMessage(case["query"])]) if len(ai_msg.tool_calls) == 0: print(f"FAIL: {case['description']} - no tool called") failures += 1 continue actual_tool = ai_msg.tool_calls[0]["name"] actual_args = ai_msg.tool_calls[0]["args"] if actual_tool != case["expected_tool"] or actual_args != case["expected_args"]: print(f"FAIL: {case['description']} - got {actual_tool} {actual_args}") failures += 1 print(f"Passed {len(test_cases) - failures}/{len(test_cases)} tests") run_tests()

这段脚本在验证两件事:模型是否选择了正确的工具、是否传了正确的参数。两者都符合才算通过。量化的验证远比人工抽查可靠,尤其在修改了工具描述或模型版本之后。

日志策略同样关键。工具调用日志要记录:查询原文、模型选中的工具名、参数、工具返回结果、最终回答。出问题时靠这些日志回溯是哪一层出了问题——是模型选错工具、参数解析失败还是工具执行出错。日志是 Agent 开发的显微镜,没有日志就是盲调。

一个我吃过亏的经历:某次改了一个工具的描述措辞,本以为只是「更清楚了一点」,结果模型开始动不动就调这个工具,把查询类任务全带偏了。从那以后我每次改动工具定义或描述,都强制完整跑一遍回归测试集,再上线到真实环境。先跑测试,再看单例效果,才不算踩进「看似变好实则变坏」的坑里。希望这些方法对你调试自己的 Agent 系统有帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询