简介《Building Applications with AI Agents构建智能体应用》是Michael Albada撰写、OReilly Media出版的AI代理开发指南以早期发布电子书形式呈现未编辑完成的内容适合想掌握多智能体系统设计的技术开发者。这份PDF为单文件压缩包仅558KB方便快速下载但需注意它是预览版目前目录内仅“技能”与“编排”两章完整可用其余章节标注暂缺。书中系统讲解智能代理在软件工程中的应用涵盖代理间通信协作的架构设计、环境感知与决策机制、机器学习与模式识别相关技术以及容错性、可扩展性与现实世界接口设计。读者还能看到自动化服务、智能调度、网络安全等行业的应用案例并了解开源许可与知识产权合规要求从而建立从单一代理到多代理协同的完整认知。该资源已有437人学习适合追踪AI代理前沿技术、提前熟悉OReilly新书框架的开发者参考。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_namegpt-4o, temperature0) llm_with_tools llm.bind_tools(tools)这段代码逻辑上分三步定义工具函数、组装工具列表、绑定到 LLM。tool装饰器会把普通 Python 函数变成 LangChain 可识别的工具对象函数的docstring会被当作工具描述传给模型所以描述必须写清楚「这个工具是干嘛的」和「参数是什么意思」模型才能正确选择。temperature0是刻意为之的——工具调用场景下我们希望模型输出确定性最高不要有随机性干扰参数选择。接下来是调用的核心循环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 fhttps://api.example.com/stocks/{ticker} response requests.get(api_url) if response.status_code 200: data response.json() return data[price] else: raise ValueError(fFailed to fetch stock price for {ticker}) llm ChatOpenAI(model_namegpt-4o, temperature0) 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_results1, doc_content_chars_max3000) tool WikipediaQueryRun(api_wrapperapi_wrapper) llm ChatOpenAI(model_namegpt-4o, temperature0) 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加AgentExecutorfrom 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(agentagent, toolstools, verboseTrue) result executor.invoke({input: Calculate 15 * 7 and then fetch the price of AAPL})agent_scratchpad是 LangChain 内部维护的中间步骤记录AgentExecutor 会自动处理工具调用、结果追加、循环终止。verboseTrue可以观察每次工具调用的细节很适合开发调试。如果模型在循环里反复调用同一个工具停不下来或者工具链过长就需要设置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工具直接ValueErrorAgent 报错退出。原因是函数没有做输入清洗默认调用方会传合法参数。解决是在工具函数入口加类型转换和边界检查把非法输入转换成可处理的默认值或返回明确错误信息。但要注意别「吞掉」错误工具内部尽量做防御性编程让错误显性化但不要让 Agent 崩溃。一个实际做法是让工具返回结构化结果{ok: True, data: ...}或{ok: False, error: ...}模型才能知道下一步该怎么办。5.3 温度参数从没调过Agent 工具选择不稳定现象是同一句话问三次模型有时调工具有时不调工具。原因是没有设置temperature默认值较高模型输出随机性强。解决方法是 Agent 场景设temperature0必要时用top_p控制采样范围。原理是工具选择属于「必须确定性」的决策随机性只会带来不稳定的行为。但也不能一刀切——如果 Agent 同时负责生成文案可以在最终生成环节单独用一个高温度模型工具调用与文案生成解耦。5.4 多工具循环没有终止条件token 烧到爆现象是 Agent 在一个错误答案上反复调用工具形成死循环。原因是没有设置迭代上限或者工具错误信息不够清晰模型在「重试 → 失败 → 重试」里打转。解决方法是设置max_iterations5左右的上限同时让工具在遇到明确错误时返回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_namegpt-4o, temperature0) 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: Whats 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(fFAIL: {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(fFAIL: {case[description]} - got {actual_tool} {actual_args}) failures 1 print(fPassed {len(test_cases) - failures}/{len(test_cases)} tests) run_tests()这段脚本在验证两件事模型是否选择了正确的工具、是否传了正确的参数。两者都符合才算通过。量化的验证远比人工抽查可靠尤其在修改了工具描述或模型版本之后。日志策略同样关键。工具调用日志要记录查询原文、模型选中的工具名、参数、工具返回结果、最终回答。出问题时靠这些日志回溯是哪一层出了问题——是模型选错工具、参数解析失败还是工具执行出错。日志是 Agent 开发的显微镜没有日志就是盲调。一个我吃过亏的经历某次改了一个工具的描述措辞本以为只是「更清楚了一点」结果模型开始动不动就调这个工具把查询类任务全带偏了。从那以后我每次改动工具定义或描述都强制完整跑一遍回归测试集再上线到真实环境。先跑测试再看单例效果才不算踩进「看似变好实则变坏」的坑里。希望这些方法对你调试自己的 Agent 系统有帮助。本文还有配套的精品资源点击获取