☰
AI Agent技能设计:本地硬编码与API外联的工程化实践
2026/10/23 3:06:41 网站建设 项目流程

简介:本资源为O’Reilly早期发布的电子书《Building Applications with AI Agents》PDF预览版,面向AI工程实践者、系统架构师及对多智能体系统(MAS)有进阶需求的开发者。内容聚焦AI Agent的设计原理与工程落地,覆盖技能封装、任务编排、记忆机制、多Agent协同、生产环境监控与安全防护等核心模块,特别适合希望在大模型应用层构建可扩展、可验证智能系统的工程师学习参考。资源为单文件PDF,大小558KB,轻量便携,便于快速通读与重点章节精研。目前已有441人下载学习,虽属Early Release未完结版本(当前仅开放第3章‘Skills’与第4章‘Orchestration’),但已包含作者Michael Albada对Agent系统抽象、接口设计及真实场景集成的关键思考,附带版权说明、开源合规提示与出版社免责条款,具备技术前瞻性与工程警示价值。

1. 为什么你写的第一个 AI Agent 总是“能跑但不敢上线”:从技能设计开始重建可信度

你写完第一个调用multiply工具的 AI Agent,print(final_response.content)输出了正确结果,心里一松——任务完成了。但当导师问“如果用户输入393 * 12.25 + 7呢?”,或者测试同学随手发来一句“把上个月销售数据按区域汇总,再画个柱状图发到钉钉群”,你立刻卡住:不是模型不会,是你的 Agent 根本没被设计成能接住这种真实请求的系统。这不是能力问题,是结构问题。这本书《Building Applications with AI Agents》的第三章“Skills”直击要害:AI Agent 的可靠性不取决于 LLM 多强,而取决于你给它装了多少可验证、可组合、可降级的技能模块。它不教你怎么调大 temperature,而是手把手带你把“加减乘除”“查维基”“取股价”这些动作,拆成独立开发、单独测试、明确边界、带错误兜底的工程单元。适合正在做毕设/课设、需要交付一个“真能干活”的多 Agent 系统的学生;也适合刚接手内部 AI 工具链建设的工程师——你不需要从零造轮子,但必须知道每个轮子该长什么样、怎么装、装歪了会爆胎。这章内容就是你方案文档里“核心模块设计”那一节的原始蓝本,不是概念堆砌,全是可抄、可改、可答辩的实战切口。


2. 技能分层设计:本地硬编码技能与 API 外联技能的职责边界与协同逻辑

AI Agent 的技能不是功能列表,而是责任契约。一个合格的技能模块,必须清晰回答三个问题:它只负责什么(职责)、失败时怎么办(容错)、和其他技能怎么配合(编排)。本章将技能划为两大类:本地硬编码技能(Hand-crafted Local Skills)和API 外联技能(API-Based Skills),它们不是技术选型偏好,而是工程权衡的结果——前者保确定性,后者扩可能性。下面从设计意图、实现范式、集成方式三方面展开,每部分都附可直接粘贴进你毕设代码的最小可行示例,并说明关键参数为何这样设。

2.1 本地硬编码技能:用确定性锚定 Agent 的可信基线

本地技能的核心价值,在于解决大模型最不擅长却最常被要求的事:精确计算、状态保持、规则执行。比如单位换算、日历推算、图遍历路径生成——这些任务输入输出明确、逻辑封闭、不容歧义。若全交给 LLM 自由发挥,轻则结果漂移(393 * 12.25算成4814.249999),重则引发生产事故(金融系统中金额四舍五入错误)。因此,本地技能的设计原则是:显式定义、无副作用、可单测、有兜底。

以书中提供的计算器技能为例,其 LangChain 实现看似简单,但每行代码都有工程深意:

from langchain_core.tools import tool from langchain_core.runnables import ConfigurableField @tool def multiply(x: float, y: float) -> float: """Multiply 'x' times 'y'.""" # 关键1:类型强制校验,拒绝字符串或None输入 if not isinstance(x, (int, float)) or not isinstance(y, (int, float)): raise ValueError("Both x and y must be numbers") # 关键2:精度控制,避免浮点误差累积影响下游 result = round(x * y, 10) return result @tool def add(x: float, y: float) -> float: """Add 'x' and 'y'.""" if not isinstance(x, (int, float)) or not isinstance(y, (int, float)): raise ValueError("Both x and y must be numbers") result = round(x + y, 10) return result

提示:为什么必须加round(..., 10)?
Python 浮点数存在二进制表示误差(如0.1 + 0.2 != 0.3)。在金融、科学计算等场景,Agent 若将0.30000000000000004传给下游系统,可能触发校验失败。round是最轻量的精度锚定手段,比引入decimal模块更适合作为毕设基础技能——它不增加依赖,却堵住了最常见的一类“玄学翻车”。

这些函数被注册为 LangChaintool后,LLM 调用时会自动进行参数类型校验和 JSON Schema 生成。但注意:LLM 不会自动处理你代码里的ValueError。若用户输入multiply("three", 5),LLM 仍会尝试调用并抛出异常,导致整个链路中断。因此,本地技能必须自带防御式编程:

@tool def safe_multiply(x: str, y: str) -> str: """Multiply two numbers given as strings. Returns string result or error message.""" try: # 尝试转为数字,支持整数和小数字符串 num_x = float(x) if '.' in x else int(x) num_y = float(y) if '.' in y else int(y) result = round(num_x * num_y, 10) return str(result) except (ValueError, TypeError) as e: return f"Error: Invalid input '{x}' or '{y}' — please enter numbers only."

这个safe_multiply版本将错误转化为可读字符串返回,LLM 能将其自然融入最终回复(如:“抱歉,我无法计算‘three’乘以5,因为‘three’不是数字”),而非让整个 Agent 崩溃。这是你在毕设答辩时展示“鲁棒性设计”的关键细节——评审老师一眼就能看出你考虑了真实用户乱输的场景。

2.2 API 外联技能:用服务契约替代信任幻觉

当任务超出本地计算范畴(查实时天气、读公司知识库、调用审批流),就必须引入 API 技能。但这里有个致命误区:很多学生直接把requests.get(url)包一层@tool就完事,结果上线后发现:网络超时没处理、API 返回 404 时 Agent 哑火、JSON 解析失败就报红…… 这不是技能,这是定时炸弹。API 技能的本质是服务契约封装——它必须明确定义:我能提供什么(成功响应)、我可能失败在哪(错误码)、失败时我能告诉你什么(降级信息)。

书中给出的股票价格查询技能,其原始代码存在明显工程缺陷:

@tool def get_stock_price(ticker: str) -> float: """Get the stock price for the stock exchange ticker.""" 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") # ❌ 单一错误提示,无分类

这段代码在毕设演示中可能蒙混过关,但实际部署会暴露三大问题:

  1. 无超时控制:requests.get默认永不超时,一次网络抖动就卡死整个 Agent;
  2. 错误粒度太粗:404(股票代码不存在)和503(服务不可用)混为一谈,无法针对性处理;
  3. 无降级策略:失败时只能抛异常,无法返回“暂无数据,请稍后再试”这类友好提示。

修正后的生产级写法如下:

import requests from typing import Optional, Dict, Any @tool def get_stock_price(ticker: str) -> str: """ Get current stock price for a given ticker symbol. Returns formatted string with price or descriptive error. """ # 关键1:显式超时,防止阻塞 timeout = (3.05, 10) # connect timeout 3.05s, read timeout 10s # 关键2:构造健壮 URL,防御注入 if not ticker.isalnum() or len(ticker) > 10: return f"Error: Invalid ticker '{ticker}'. Please use alphanumeric symbols up to 10 chars." api_url = f"https://api.example.com/stocks/{ticker.upper()}" try: response = requests.get(api_url, timeout=timeout) # 关键3:按 HTTP 状态码分类处理 if response.status_code == 200: data = response.json() price = data.get("price") if price is not None: return f"${float(price):.2f}" # 格式化为货币字符串 else: return "Error: Price data missing in API response" elif response.status_code == 404: return f"Error: Stock '{ticker}' not found. Please check the symbol." elif response.status_code in [429, 503]: return "Error: Service temporarily unavailable. Please try again later." else: return f"Error: API returned status {response.status_code}. Details: {response.text[:100]}" except requests.exceptions.Timeout: return "Error: Request timed out. Network may be slow or service unreachable." except requests.exceptions.ConnectionError: return "Error: Failed to connect to stock service. Check network or service status." except ValueError as e: # JSON decode error return f"Error: Invalid response format from API: {str(e)}" except Exception as e: return f"Unexpected error: {str(e)}"

注意:返回类型为何从float改为str?
LangChain 的tool机制要求所有技能返回值必须能被序列化为 JSON(供 LLM 解析)。float虽然可序列化,但一旦发生NaN或inf,会直接崩溃。而str类型天然安全,且能承载丰富语义(成功结果、结构化错误、友好提示)。LLM 天然擅长处理文本,把“错误”也变成它能理解的文本,是提升用户体验的关键设计选择——这正是书中强调的“技能即接口契约”的落地体现。

2.3 两类技能的协同编排:为什么不能让 LLM 自己决定调用顺序

很多初学者认为:“只要我把add、multiply、get_stock_price全注册成工具,LLM 自会聪明地组合它们”。这是最大的认知陷阱。LLM 的工具调用本质是单步决策:它根据当前消息,预测下一步该调哪个工具、传什么参数。它没有全局状态记忆,无法理解“先查股价 A,再查股价 B,最后算差价”这样的多步流程。若强行依赖 LLM 编排,会出现经典翻车场景:

  • 用户问“苹果和微软股价差多少?”,LLM 可能先调get_stock_price("AAPL"),但拿到结果后忘记存起来,接着调get_stock_price("MSFT")时,又把 AAPL 结果丢了;
  • 或更糟:它同时发起两个 API 调用,但 LangChain 默认是串行执行,第二个调用会等第一个完成,白白浪费时间。

解决方案是显式引入编排层(Orchestration Layer),这正是本书第四章的核心。但在技能设计阶段,你就该为编排铺路:

  • 本地技能应尽量无状态、幂等(如add(2,3)每次都返回5);
  • API 技能应返回结构化数据,而非纯文本(如{ "symbol": "AAPL", "price": 192.34, "timestamp": "2025-04-05T10:30:00Z" }),方便后续技能解析;
  • 为高频组合任务预置复合技能(如get_stock_comparison(symbol_a, symbol_b)),内部封装两次 API 调用+计算逻辑,对外暴露单一接口。

这并非过度设计。在你的毕设中,一个预置的compare_two_stocks技能,比让 LLM 尝试自己组合两个独立技能,成功率高 3 倍以上,且代码可测试、可调试、可向答辩老师清晰解释设计逻辑。


3. 技能开发避坑指南:5 个让毕设答辩当场沉默的典型翻车点

写技能代码容易,写出能在答辩现场稳定运行、经得起老师连环追问的技能很难。以下是我在带多个课程设计项目时,学生踩得最多、最影响评分的 5 个坑。每条都按“现象→原因→解决”给出可立即执行的补救方案,照着改,你的 Demo 稳如磐石。

3.1 现象:LLM 死循环调用同一个工具,CPU 占满

原因:工具返回结果未被正确解析,或 LLM 对工具输出的理解存在歧义,导致它认为“还没完成”,反复调用同一工具。例如,get_stock_price("AAPL")返回"$192.34",但 LLM 期望看到"price": 192.34的 JSON,于是它觉得“数据格式不对”,再调一次。
解决:

  • 在工具函数内强制返回LangChain 兼容的ToolMessage或标准字典,而非原始字符串;
  • 为关键工具添加description字段,明确告诉 LLM 返回格式(LangChain 会将其注入 system prompt):
@tool def get_stock_price(ticker: str) -> dict: """Get stock price. Returns dict with keys 'symbol', 'price', 'currency'.""" # ... 实现同前 ... return { "symbol": ticker.upper(), "price": float(price), "currency": "USD" }

3.2 现象:本地技能在 Windows 上正常,Linux 服务器部署时报ModuleNotFoundError

原因:本地开发时用了pip install -e .安装自定义包,但生产环境未安装,或路径未加入PYTHONPATH。更隐蔽的是,某些技能依赖os.system()调用 shell 命令(如curl),而服务器未安装对应命令。
解决:

  • 所有本地技能必须零外部依赖:禁用os.system,subprocess.Popen(除非你明确打包了所有依赖二进制);
  • 使用requirements.txt显式声明所有依赖(包括langchain-core==0.3.0这种精确版本);
  • 在技能函数开头加环境检查:
import sys if sys.platform != "linux": raise RuntimeError(f"Skill 'linux_only_tool' requires Linux, but running on {sys.platform}")

3.3 现象:API 技能在本地 Postman 测试成功,集成到 Agent 后 401 Unauthorized

原因:API Key 未正确注入。学生常犯两种错:一是把 Key 写死在代码里(Git 提交泄露风险),二是用os.getenv("API_KEY")但忘记在运行环境设置该变量。
解决:

  • 永远不要在代码里写死密钥。使用 LangChain 的ConfigurableField动态注入:
@tool def get_weather(city: str, api_key: str = ConfigurableField(default="")) -> str: """Get weather for city. api_key is injected at runtime.""" if not api_key.strip(): return "Error: Weather API key not configured. Contact admin." # ... 使用 api_key 调用 ...
  • 在主程序中统一配置:
llm_with_tools = llm.bind_tools([get_weather]).with_config( configurable={"get_weather_api_key": "your_actual_key_here"} )

3.4 现象:用户问“昨天的股价”,技能返回“Error: Invalid date format”

原因:技能参数类型过于僵硬。get_stock_price只接受strticker,但用户自然语言中常含时间(“昨天”、“上个月”、“2024年Q3”),LLM 无法自动提取标准化时间戳。
解决:

  • 技能不承担 NLU(自然语言理解)责任。NLU 应由前置的“意图识别”模块完成;
  • 技能接口只接收标准化参数,并在description中明确约束:
@tool def get_historical_stock_price(ticker: str, date: str) -> str: """Get stock price for a specific date. Date format: YYYY-MM-DD (e.g., '2024-01-01').""" # ... 验证 date 格式 ... if not re.match(r"^\d{4}-\d{2}-\d{2}$", date): return f"Error: Invalid date '{date}'. Use format YYYY-MM-DD."
  • 在你的毕设方案中,明确写出“意图识别模块将用户输入‘昨天的股价’解析为{'ticker': 'AAPL', 'date': '2025-04-04'}”,这就是架构设计的体现。

3.5 现象:多个技能并发调用时,日志混乱,无法定位哪个技能出错

原因:所有技能共用同一日志器,无上下文标识。当multiply和get_stock_price同时执行,日志里只看到INFO: Error occurred,不知是哪个。
解决:

  • 为每个技能创建独立 logger,名称包含技能名:
import logging logger = logging.getLogger(f"skill.{multiply.__name__}") @tool def multiply(x: float, y: float) -> float: logger.info(f"Called with x={x}, y={y}") try: result = x * y logger.info(f"Success: {result}") return result except Exception as e: logger.error(f"Failed: {e}", exc_info=True) raise
  • 在主程序初始化时配置日志格式,包含%(name)s:
logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" )

这样日志会清晰显示2025-04-05 10:30:00,123 - skill.multiply - ERROR - Failed: ...,答辩时老师问“哪里报错”,你秒答:“看日志第三行,multiply 技能在计算时遇到类型错误”。


4. 技能模块化验收标准:用 4 张表定义你的毕设“及格线”

毕设不是写完代码就结束,而是要证明你写的每个技能都可验证、可替换、可演进。以下是我给某高校课程设计设定的硬性验收表,你可直接套用。每张表对应一个维度,填满即达标。别怕表格多——答辩老师最爱看结构化证据,这比你说“我做了很多优化”有力十倍。

4.1 技能功能清单表(必交)

技能 ID技能名称类型(本地/API)输入参数(含类型/约束)输出格式是否幂等是否有降级返回对应毕设需求编号
SK-01multiply本地x: float,y: floatfloat(四舍五入10位)✅❌(已内置ValueError)REQ-CALC-01
SK-02get_wiki_summaryAPIquery: str(≤50字符)str(摘要文本)✅✅(404时返回“未找到”)REQ-INFO-02
SK-03send_to_dingtalkAPImsg: str,group_id: strstr(“已发送”/错误详情)❌(发送是副作用)✅(网络失败返回“发送失败”)REQ-NOTIFY-03

说明:此表证明你理解“技能即接口”,每个字段都是工程基本功。是否幂等列尤其重要——它直接关联到系统可靠性设计。答辩时老师若问“如果网络抖动导致send_to_dingtalk被重复调用三次,会发生什么?”,你能指着这一栏说“我们设计为非幂等,但通过消息去重ID确保业务上只生效一次”,瞬间拉满专业感。

4.2 技能单测覆盖率表(必交)

技能 ID测试用例数成功路径覆盖失败路径覆盖(如输入错误、API 404/503)边界值测试(如空字符串、极大数)行覆盖率(pytest-cov)备注
SK-015✅(3个)✅(2个:x=None,y="abc")✅(x=1e100,y=1e-100)98%使用pytest
SK-024✅(2个)✅(2个:query="",query="very_long_query_...")✅(长度50字符临界)92%MockWikipediaAPIWrapper
SK-033✅(1个)✅(2个:requests.exceptions.Timeout,401 Unauthorized)—85%Mockrequests.post

说明:大学课程设计不要求 100% 覆盖率,但必须覆盖所有失败分支。这是区分“能跑”和“可靠”的分水岭。你只需在tests/目录下放 3 个test_skills.py文件,每份 10 行左右,就能填满此表。答辩时打开 VS Code 展示测试通过截图,比讲十分钟原理更直观。

4.3 技能性能基线表(建议提交)

技能 ID本地技能(平均耗时 ms)API 技能(P95 耗时 ms)并发 10 QPS 下 CPU 占用内存增长(100次调用)备注
SK-010.02—<1%无增长纯计算
SK-02—120015%+2MB依赖网络
SK-03—8508%+1.5MB含 JSON 序列化

说明:用timeit或locust做简单压测即可。此表证明你考虑了“规模”——当你的 Agent 从单人 Demo 扩展到小组使用时,性能不会断崖下跌。老师若问“如果 50 个同学同时用,系统会不会卡?”,你拿出这张表,指着 SK-02 的 P95 1200ms 说:“在 10 QPS 下仍低于 1.5 秒,满足教学场景响应要求”,非常有说服力。

4.4 技能演进路线表(加分项)

技能 ID当前版本下一迭代目标依赖条件预估工作量(小时)备注
SK-01v1.0支持复数运算multiply_complex(a+bi, c+di)学习cmath库2拓展数学能力
SK-02v1.0支持多语言摘要(lang="zh"参数)接入翻译 API4提升国际化
SK-03v1.0支持图片消息发送DingTalk API 升级3增强通知形式

说明:此表展现你的工程视野。毕设不是终点,而是起点。列出 2-3 个合理、可实现的演进点,证明你理解软件生命周期。答辩时老师若说“这个设计很完整”,你可以顺势说:“是的,我们已规划好 V2 版本,比如为 SK-02 加入多语言支持,这只需要对接一个翻译 API,工作量可控”,显得既有深度又有远见。


5. 毕设答辩通关技巧:用“技能沙盘推演”让老师主动追问你的设计

答辩不是背稿,而是引导老师进入你的设计世界。我带过的最成功的毕设,学生没讲一行代码,而是用一张白纸,现场画了一个“技能沙盘”,把整个 Agent 的运作过程拆解成可触摸的模块。这套方法,我称之为“三阶沙盘推演法”,专治答辩冷场、老师走神、问题答偏。它不靠背诵,靠设计思维的自然流露。

5.1 第一阶:静态沙盘——画出技能的“物理存在”

拿一支笔,在白纸上画三个区域:

  • 左区(技能仓库):画几个方框,标上SK-01 multiply、SK-02 get_wiki、SK-03 send_dingtalk。在每个框里,用小字写上它的类型(本地/API)、输入约束(如x,y: float)、输出承诺(如str with price)。
  • 中区(LLM 大脑):画一个云朵形状,写上GPT-4o。从每个技能框画箭头指向云朵,标注bind_tools()。
  • 右区(外部世界):画一个地球图标代表互联网,旁边写Wikipedia API、DingTalk Webhook;画一个计算器图标代表本地,写Python math。

为什么有效?
这张图瞬间把抽象的“Agent”变成具象的“系统”。老师一眼看清:哪些是你的代码(左区)、哪些是黑匣子(中区)、哪些是外部依赖(右区)。当老师问“这个系统怎么保证安全?”,你手指左区:“所有技能都经过输入校验,比如 SK-01 会拒绝非数字输入,这是我们的第一道防线”。

5.2 第二阶:动态沙盘——推演一次真实请求的“心跳”

请老师给你一个具体问题,比如:“查一下特斯拉今天股价,然后发到钉钉群”。你拿起另一支彩笔,边画边说:

  1. 第一步(红色箭头):用户输入 → LLM 云朵 → LLM 判断需调用SK-02 get_stock_price(指向左区);
  2. 第二步(蓝色箭头):SK-02→ 地球图标 → 调用https://api.example.com/stocks/TSLA;
  3. 第三步(绿色箭头):API 返回{"price": 172.45}→SK-02解析 → 返回"172.45"给 LLM;
  4. 第四步(紫色箭头):LLM 拿到价格,再调用SK-03 send_dingtalk→ 地球图标 → 发送消息。

关键动作:在每一步,你停顿半秒,问老师:“这一步,您觉得哪个环节最容易出问题?”
如果老师说“API 调用失败”,你立刻指向SK-02框,翻开你的技能代码,展示except requests.exceptions.Timeout:那段——把问题引向你准备最充分的地方。这比被动回答“你怎么处理超时”高明十倍。

5.3 第三阶:压力沙盘——挑战极限,暴露设计深度

主动提出一个极端场景:“如果钉钉服务宕机了,整个 Agent 是不是就瘫痪了?”

  • 你擦掉SK-03到地球的箭头,画一个叉;
  • 在SK-03框里加一行小字:“降级策略:记录日志 + 返回‘通知发送失败,已存草稿’”;
  • 指向你的技能功能清单表,说:“所以我们在表里明确写了‘是否有降级返回’,SK-03 是✅,这意味着即使外部服务不可用,Agent 仍能完成核心任务(查股价),只是通知功能降级。”

血泪经验:老师最喜欢问“如果 XX 失败了怎么办”。提前准备 3 个这样的压力场景(网络失败、LLM 返回格式错误、本地技能抛异常),并在沙盘上用不同颜色标记应对策略。当你能从容画出“故障树”,老师会默认你已通过了工程素养的终极考核。

从那以后我每次带毕设,都强制学生在答辩前用 A4 纸手绘三遍这个沙盘——第一遍练布局,第二遍练讲解节奏,第三遍练应对追问。不是为了表演,而是让设计逻辑真正长进肌肉记忆。当你的手指能自然指向图纸上那个小小的SK-01框,说出“这里我们用round(x*y, 10)锚定精度”,你就已经赢了。希望帮到你。

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

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

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

立即咨询