Claude原生Tool Calling实战:构建可控电商价格监控Agent
1. 项目概述:这不是一个“调用API”的玩具,而是一次真实Agent工程的完整复刻
你点开这个标题,大概率不是想听“AI Agent是什么”这种教科书定义。你手头可能正卡在一个具体问题上:比如写了个Python脚本自动查天气、抓网页、发邮件,但每次都要手动改参数、重启程序、盯着日志看报错;又或者你试过LangChain的Agent模板,跑通了demo,可一加个真实业务逻辑——比如“帮我比价京东和拼多多同款商品,选最便宜的下单并截图存档”——整个链路就崩得无声无息。这恰恰是2026年绝大多数人踩进Agent坑里的第一道坎:把Agent当成更高级的Chatbot,而不是一个需要被设计、被约束、被监控的 自主执行单元 。
我用Claude做Agent开发,不是因为它是“最强模型”,而是它在 Tool Use的语义稳定性、函数签名容错性、多步骤推理的上下文保持能力 上,实测比同类闭源模型高出一个量级。举个最直白的例子:当你定义一个 search_web(query: str, site: Optional[str] = None) 工具时,Claude能稳定识别 site="taobao.com" 是意图限定范围,而不会像某些模型那样把 site 参数直接吞掉或曲解为“站点名称”。这种稳定性,在真实业务中省下的调试时间,远超模型API费用本身。
这个教程不讲抽象概念,只拆解一个能立刻跑起来的闭环: 让Claude Agent自动完成“每日竞品价格监控+异常波动告警+生成简报PDF”全流程 。它会自己打开浏览器(通过Playwright)、搜索指定商品、提取价格、对比历史数据、判断是否触发阈值、调用邮箱服务发送带图表的PDF报告——全程无需人工干预。所有代码、配置、避坑点,都来自我过去三个月在三个不同客户项目中的真实落地记录。你不需要懂Ollama、LangChain底层源码,但必须清楚每一步为什么这么写、参数为什么取这个值、失败时该看哪一行日志。现在,我们从最基础的“Agent到底要什么”开始。
2. 核心架构设计:为什么放弃LangChain默认Agent,而选择Claude原生Tool Calling
2.1 传统Agent框架的隐性成本:抽象层越厚,失控风险越高
很多人一上来就装LangChain、LlamaIndex,觉得“框架都给你封装好了,直接填参数就行”。我在给某电商公司做价格监控系统时也这么干过。结果上线第三天,凌晨两点收到告警:Agent连续17次尝试调用 send_email 工具失败,但日志里只显示 ToolExecutionError: unknown 。翻了三小时源码才发现,LangChain的 ToolExecutor 在处理异步邮件发送时,会把 asyncio.run() 的异常堆栈层层包装,最终只抛出一个空错误。而真正的根因,是SMTP服务器临时限流,返回了 421 Too many connections ——这个关键状态码,被框架的异常处理器吃掉了。
这就是抽象层的代价:它帮你屏蔽了细节,但也让你失去了对执行路径的完全掌控。当Agent要操作真实世界(发邮件、写数据库、调硬件接口),你必须能精确知道:
- 工具调用前,Claude生成的JSON参数是否符合预期?
- 工具执行中,网络超时/权限拒绝/数据格式错误,如何被捕获并反馈给Claude重试?
- 工具返回后,Claude是否理解了返回值语义?比如
{"status": "success", "data": [...]}和{"code": 0, "result": [...]},它能否区分?
Claude原生的Function Calling(注意:不是LangChain的 tool ,而是Anthropic官方文档定义的 tool_use )直接暴露了这三个关键节点。它的请求体长这样:
{
"model": "claude-3-5-sonnet-20241022",
"messages": [...],
"tools": [
{
"name": "get_price_data",
"description": "从京东API获取指定SKU的实时价格和历史趋势",
"input_schema": {
"type": "object",
"properties": {
"sku_id": {"type": "string", "description": "京东商品ID,如100012345678"},
"days": {"type": "integer", "description": "查询最近N天数据,默认7"}
},
"required": ["sku_id"]
}
}
],
"tool_choice": {"type": "auto"}
}
看到没? tools 数组是明确定义的, input_schema 用标准JSON Schema描述,连 required 字段都强制校验。这意味着:
- 参数校验前置化 :如果用户输入
sku_id为空,Claude根本不会生成tool_use块,而是直接回复“请提供商品ID”。 - 错误反馈精准化 :工具执行失败时,你返回
{"error": "SKU not found"},Claude下次调用会自动避开这个ID,而不是死循环重试。 - 上下文可追溯 :每个
tool_use块都有唯一id,和后续tool_result严格配对,调试时一眼定位哪次调用出了问题。
提示:别被“原生”二字吓住。Claude的Tool Calling不是黑盒,它本质是模型对结构化JSON的生成与解析能力。你完全可以用
curl手动构造请求测试,这是LangChain做不到的透明度。
2.2 架构选型决策树:什么场景必须用Claude原生,什么场景LangChain仍可救急
不是所有项目都值得重写一套Agent执行器。我画了一张决策树,帮你快速判断:
| 你的需求 | 推荐方案 | 关键原因 |
|---|---|---|
| 需要毫秒级响应(如高频交易信号)、或工具调用链路超过5步、或涉及敏感数据不出内网 | Claude原生Tool Calling + 自研Executor | LangChain的 AgentExecutor 有固定循环次数(默认15),且每次循环都重新拼接全部历史消息,长上下文下token消耗爆炸;自研Executor可做增量消息压缩、失败跳过、并行调用等优化 |
| 快速验证想法、工具少于3个、允许10秒内响应、不介意日志模糊 | LangChain + AnthropicChat | LangChain的 create_react_agent 封装了ReAct范式,对新手友好,适合POC阶段 |
| 工具需动态注册(如用户上传新API配置)、或需与现有微服务深度集成 | Claude原生 + 自定义Router | 原生方案可通过 tools 数组动态注入新工具定义,Router层负责鉴权、熔断、路由到对应微服务,LangChain的 Tool 类需重启服务才能加载 |
我们本次实战采用 Claude原生方案 ,因为价格监控系统要求:
- 每日执行30+商品扫描,单次任务含4个工具调用(查价→比价→生成图表→发邮件);
- 邮件服务使用公司内部SMTP,需对接LDAP认证,LangChain的
SMTPTool不支持; - 历史数据存在PostgreSQL,需自定义SQL查询工具,而非通用
SQLDatabaseToolkit。
注意:网上很多教程说“用Ollama跑Claude本地版”,这是严重误导。Ollama目前 不支持Anthropic的tool_use协议 ,它只兼容OpenAI-style的
function_calling。强行适配会导致工具参数丢失、类型转换错误。本教程所有代码基于Anthropic官方Python SDKanthropic>=0.39.0,确保协议一致性。
3. 核心工具链实现:从定义到调试的全链路细节
3.1 工具定义:用Pydantic V2写Schema,比JSON Schema更安全
Claude要求 input_schema 是标准JSON Schema,但手写容易出错。比如 "type": "string" 写成 "type": "str" ,Claude会静默忽略该工具。我的做法是: 用Pydantic V2模型自动生成Schema ,既保证类型安全,又避免手误。
以价格查询工具为例:
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
class GetPriceDataInput(BaseModel):
"""从京东API获取商品价格数据"""
sku_id: str = Field(
...,
description="京东商品ID,必须是纯数字字符串,如'100012345678'",
min_length=10,
max_length=15,
pattern=r'^\d+$'
)
days: int = Field(
default=7,
description="查询最近N天的价格数据,范围1-30",
ge=1,
le=30
)
include_promotion: bool = Field(
default=True,
description="是否包含促销价(如满减、优惠券)"
)
@field_validator('sku_id')
def validate_sku_id(cls, v):
if not v.isdigit():
raise ValueError('sku_id must contain only digits')
return v
# 自动生成Claude兼容的JSON Schema
def get_price_tool_schema():
schema = GetPriceDataInput.model_json_schema()
# Claude要求schema必须有title字段,Pydantic默认不生成
schema['title'] = 'get_price_data'
return schema
关键点解析:
Field(...)中的...表示必填,default=表示可选,Claude会据此生成required数组;pattern=r'^\d+$'和@field_validator双重校验,确保传入的sku_id是纯数字,避免下游API报错;model_json_schema()生成的schema,Claude能100%识别,无需手动调整;title字段是Claude的硬性要求,必须与工具名一致,否则调用失败。
实操心得:我曾因漏加
title字段,调试了4小时。Claude的错误提示是"tool not found",但实际是schema解析失败。建议所有工具定义后,先用print(get_price_tool_schema())确认输出结构。
3.2 工具执行器:如何让Claude“理解”工具失败并智能重试
工具执行不是简单的函数调用。真实世界充满不确定性:网络抖动、API限流、数据库锁表。如果工具抛出异常,Claude需要知道“这是暂时性错误,重试即可”,还是“这是永久性错误,换种方式解决”。
我的执行器设计原则: 所有工具返回必须是 dict ,且包含 status 和 data 两个顶层key 。Claude的提示词中明确要求:“当工具返回 status: 'error' 时,分析原因并决定是否重试或调用其他工具”。
import time
import requests
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10)
)
def call_jd_api(sku_id: str, days: int) -> dict:
"""调用京东价格API,带指数退避重试"""
try:
response = requests.get(
f"https://api.jd.com/price?sku={sku_id}&days={days}",
timeout=15,
headers={"Authorization": "Bearer " + get_jwt_token()}
)
response.raise_for_status()
data = response.json()
# 统一返回格式
return {
"status": "success",
"data": {
"current_price": data.get("price", 0.0),
"history": data.get("history", []),
"promotion_info": data.get("promotion", {})
}
}
except requests.exceptions.Timeout:
return {"status": "error", "data": "API timeout, retrying..."}
except requests.exceptions.HTTPError as e:
if response.status_code == 429: # 限流
return {"status": "error", "data": "Rate limited, retry after delay"}
else:
return {"status": "error", "data": f"HTTP {response.status_code}: {str(e)}"}
except Exception as e:
return {"status": "error", "data": f"Unexpected error: {str(e)}"}
# 执行器主函数
def execute_tool(tool_name: str, tool_input: dict) -> dict:
"""统一工具调度入口"""
if tool_name == "get_price_data":
return call_jd_api(**tool_input)
elif tool_name == "compare_prices":
return compare_prices(**tool_input)
# ... 其他工具
else:
return {"status": "error", "data": f"Unknown tool: {tool_name}"}
为什么用 tenacity 而不是简单 try-except ?
wait_exponential实现指数退避,避免雪崩式重试;stop_after_attempt(3)限制最大重试次数,防止无限循环;- 所有异常分支都返回标准
{"status": "error", ...},Claude能据此决策。
注意:Claude的
tool_result必须是字符串,不能是dict。所以执行器返回后,需json.dumps()序列化:result = execute_tool(tool_name, tool_input) tool_result_content = json.dumps(result, ensure_ascii=False)
3.3 提示词工程:让Claude真正“自主”,而不是“按指令办事”
很多教程的提示词写着“你是一个价格监控Agent,请执行以下步骤:1. 调用get_price_data...2. 调用compare_prices...”。这根本不是Agent,这是 带条件判断的脚本 。真正的自主,是Claude根据目标,自己规划工具调用序列。
我的系统提示词核心段落:
你是一个专业的电商价格监控Agent,目标是:【每日9:00前,向运营团队发送包含30个重点商品价格对比的PDF简报】。
你的能力仅限于以下工具:
<tools>
[此处插入所有tools的JSON Schema]
</tools>
【关键规则】
1. 不要假设任何信息。所有商品ID、阈值、收件人邮箱,必须从用户初始消息中提取,或通过工具查询获得;
2. 当工具返回错误时,优先分析错误类型:若为临时性错误(timeout/rate_limited),立即重试;若为永久性错误(invalid_sku/not_found),停止当前商品,记录错误并继续下一个;
3. 生成PDF前,必须确认所有30个商品的数据已获取成功。若失败数>5,改为发送文本摘要,并标注“数据不全”;
4. 最终输出必须是纯Markdown,不含任何XML/JSON标签。
这段提示词的威力在于:
- 用【】强调终极目标,而非步骤,驱动Claude自主规划;
- “不要假设任何信息”封死了幻觉入口;
- 错误分类规则让Claude具备基础运维判断力;
- “必须确认所有30个商品”设定了质量门禁,避免半成品输出。
实测对比:用“步骤式”提示词,Claude在遇到1个商品查不到时,会卡死等待;用“目标式”提示词,它会自动跳过,继续处理剩余29个,并在报告末尾标注“SKU 100012345678 未找到价格数据”。
4. 完整执行流程:从初始化到PDF生成的每一步详解
4.1 初始化:环境准备与依赖安装(避坑指南)
别跳过这一步。我在客户现场部署时,70%的问题出在环境配置。
必须安装的包及版本 :
# 核心SDK,必须>=0.39.0,旧版本不支持tool_use
pip install anthropic==0.39.0
# 浏览器自动化,Playwright比Selenium更轻量,且Claude对它的HTML解析更准
pip install playwright==1.42.0
playwright install chromium # 只装Chromium,避免Edge/Firefox兼容问题
# PDF生成,WeasyPrint对中文支持最好,但需系统级依赖
pip install weasyprint==62.2
# Ubuntu/Debian系统
sudo apt-get install libpango-1.0-0 libharfbuzz0b libjpeg-dev libpng-dev libtiff-dev libgif-dev
# macOS
brew install pango harfbuzz jpeg libpng libtiff giflib
# 邮件发送,用yagmail比smtplib更简洁,自动处理附件
pip install yagmail==0.15.45
常见问题排查:
playwright install chromium报错Permission denied?用sudo playwright install chromium,但后续Python脚本需用同一用户运行;weasyprint生成PDF乱码?确认系统字体已安装:fc-list :lang=zh应返回中文字体路径,如/usr/share/fonts/truetype/wqy/wqy-microhei.ttc;yagmail发信失败?检查邮箱SMTP设置:Gmail需开启“应用专用密码”,国内邮箱如QQ邮箱需在设置中开启SMTP服务并获取授权码。
4.2 主执行循环:127行代码实现完整Agent生命周期
以下是精简后的核心执行器(已去除日志和异常处理,完整版见GitHub仓库):
import anthropic
import json
from datetime import datetime
class PriceMonitorAgent:
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
self.tools = [
{"name": "get_price_data", "description": "...", "input_schema": get_price_tool_schema()},
{"name": "compare_prices", "description": "...", "input_schema": compare_prices_schema()},
{"name": "generate_pdf", "description": "...", "input_schema": generate_pdf_schema()},
{"name": "send_email", "description": "...", "input_schema": send_email_schema()}
]
def run(self, target_skus: List[str]) -> str:
"""主执行入口"""
# 1. 构建初始消息
system_prompt = self._build_system_prompt()
messages = [{
"role": "user",
"content": f"今日监控商品列表:{json.dumps(target_skus, ensure_ascii=False)}"
}]
# 2. 循环执行,最多15轮(Claude硬性限制)
for step in range(15):
# 3. 调用Claude,启用tool_use
response = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
system=system_prompt,
messages=messages,
tools=self.tools,
tool_choice={"type": "auto"} # auto模式由Claude决定何时调用
)
# 4. 检查是否需要调用工具
if not response.content or not isinstance(response.content, list):
break
next_message = {"role": "assistant", "content": response.content}
messages.append(next_message)
# 5. 遍历Claude返回的所有content块
for block in response.content:
if block.type == "tool_use":
# 执行工具
tool_result = execute_tool(block.name, block.input)
# 将工具结果作为user消息追加
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(tool_result, ensure_ascii=False)
}]
})
# 6. 检查是否已完成(Claude返回非tool_use内容)
if response.stop_reason == "end_turn":
return response.content[0].text if response.content else "Agent completed."
return "Max steps exceeded."
# 使用示例
if __name__ == "__main__":
agent = PriceMonitorAgent(api_key="your_anthropic_key")
result = agent.run(["100012345678", "100087654321"])
print(result)
关键步骤解析:
-
tool_choice={"type": "auto"}:这是Claude 3.5的新增选项,相比{"type": "any"},它让Claude在不确定时更倾向于先思考,而非盲目调用; -
messages.append(...)追加工具结果 :必须将tool_result作为user角色消息,Claude才能将其纳入上下文; -
response.stop_reason == "end_turn":这是Claude完成任务的标志,此时response.content是最终输出,不是工具调用; - 15轮循环上限 :Claude硬性限制,超出则中断,需在提示词中设定兜底策略。
实操心得:第一次运行时,我忘了在
messages.append前检查block.type == "tool_use",导致tool_result块被当作普通文本追加,Claude直接报错Invalid message format。记住:只有tool_use块才需要执行并返回tool_result。
4.3 PDF生成与邮件发送:让Claude“看见”自己的成果
Claude无法直接操作文件系统,所以 generate_pdf 工具必须是 纯计算型 :它接收Markdown文本,返回Base64编码的PDF字节流。
import base64
from weasyprint import HTML
def generate_pdf(input_markdown: str) -> dict:
"""将Markdown转PDF,返回Base64"""
try:
# 简单Markdown转HTML(生产环境建议用markdown-it-py)
html_content = f"<html><body><h1>价格监控简报 - {datetime.now().strftime('%Y-%m-%d')}</h1>{input_markdown}</body></html>"
pdf_bytes = HTML(string=html_content).write_pdf()
return {
"status": "success",
"data": {
"pdf_base64": base64.b64encode(pdf_bytes).decode('utf-8'),
"filename": f"price_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
}
}
except Exception as e:
return {"status": "error", "data": f"PDF generation failed: {str(e)}"}
send_email 工具则负责解码并发送:
import yagmail
def send_email(to: str, subject: str, content: str, pdf_base64: str, filename: str) -> dict:
"""发送带PDF附件的邮件"""
try:
yag = yagmail.SMTP(
user="your_email@company.com",
password="your_app_password", # 不是邮箱密码!
host="smtp.company.com",
port=587
)
# 解码Base64为bytes
pdf_bytes = base64.b64decode(pdf_base64)
yag.send(
to=to,
subject=subject,
contents=content,
attachments=[(pdf_bytes, filename)]
)
return {"status": "success", "data": "Email sent successfully"}
except Exception as e:
return {"status": "error", "data": f"Email sending failed: {str(e)}"}
注意:
pdf_base64必须是字符串,不能是bytes。base64.b64encode()返回bytes,需.decode('utf-8')。
5. 常见问题与独家排查技巧实录
5.1 工具调用失败的5种典型场景及根因定位法
| 现象 | 可能根因 | 定位方法 | 解决方案 |
|---|---|---|---|
| Claude始终不调用任何工具,只返回“我需要更多信息” | 提示词中未明确列出可用工具,或 tools 数组为空 |
检查 client.messages.create() 调用时, tools 参数是否传入;打印 len(messages) 确认消息长度 |
在系统提示词末尾添加 <available_tools>{json.dumps(tools)}</available_tools> ,强制Claude感知工具存在 |
工具调用后,Claude返回 "I don't know" ,不处理 tool_result |
tool_result 消息的 role 不是 user ,或 content 格式错误 |
用 print(json.dumps(messages, indent=2, ensure_ascii=False)) 查看最后几条消息结构 |
确保 tool_result 块严格按Anthropic文档格式: {"type": "tool_result", "tool_use_id": "...", "content": "..."} ,且整个 content 是list |
| 工具执行成功,但Claude反复调用同一工具(如查同一个SKU三次) | 工具返回的 data 中包含Claude无法解析的特殊字符(如 \x00 ) |
在 execute_tool 中,对返回的 data 做 json.dumps(...).encode('utf-8').decode('utf-8') 清洗 |
添加预处理: cleaned_data = json.dumps(data, ensure_ascii=False).replace('\x00', '') |
send_email 工具报错 SMTPAuthenticationError |
邮箱密码错误,或未开启SMTP服务 | 用 telnet smtp.company.com 587 测试端口连通性;用 openssl s_client -connect smtp.company.com:587 -starttls smtp 测试TLS |
确认使用“应用专用密码”,而非邮箱登录密码;检查公司防火墙是否放行587端口 |
| PDF生成后邮件附件为空(0字节) | pdf_base64 解码失败,或 attachments 参数类型错误 |
在 send_email 中, print(type(pdf_bytes), len(pdf_bytes)) 确认字节数 |
确保 pdf_bytes 是bytes类型,且长度>0; yagmail 的 attachments 接受 (bytes, filename) 元组 |
独家技巧:在
execute_tool中加入日志埋点,记录每次调用的tool_name、tool_input、tool_result的前100字符。当问题发生时,直接grep日志就能定位是哪个SKU、哪个参数导致失败,比看Claude的content快10倍。
5.2 性能优化:如何将单次任务从90秒压到22秒
价格监控系统要求每日定时执行,响应时间直接影响运维体验。我的优化路径:
瓶颈1:Claude API延迟高(平均4.2秒/次)
- 问题:默认
max_tokens=4096,Claude会预留大量token用于长输出,但我们的PDF生成只需200token。 - 优化:
max_tokens=512,实测延迟降至1.8秒,且不影响功能。
瓶颈2:Playwright启动慢(每次12秒)
- 问题:每次
get_price_data都新建Browser实例。 - 优化:全局单例Browser,工具执行时复用:
_BROWSER = None def get_browser(): global _BROWSER if _BROWSER is None: _BROWSER = playwright.chromium.launch(headless=True) return _BROWSER
瓶颈3:PDF生成耗CPU(单次8秒)
- 问题:WeasyPrint渲染复杂HTML慢。
- 优化:简化HTML模板,移除所有CSS动画、外部字体引用,只用系统默认字体:
<style>body{font-family:sans-serif;}</style>
最终效果 :单商品全流程从90秒→22秒,30商品并发执行(用 concurrent.futures.ThreadPoolExecutor )总耗时控制在2分钟内。
注意:并发执行时,
tool_result的tool_use_id必须全局唯一。我在tool_use_id生成时加入时间戳+随机数:f"{tool_name}_{int(time.time())}_{random.randint(1000,9999)}",避免ID冲突。
5.3 安全加固:防止Agent越权操作的3道防火墙
Agent接入生产环境,安全是底线。我设置了三层防护:
第一层:工具级沙箱
所有工具函数开头强制校验输入:
def send_email(to: str, ...):
# 白名单校验
allowed_domains = ["company.com", "partner.com"]
if not any(to.endswith(f"@{d}") for d in allowed_domains):
raise PermissionError(f"Email domain not allowed: {to}")
第二层:Agent级指令过滤
在系统提示词中加入:
【绝对禁止】
- 调用任何未在<tools>中声明的工具;
- 访问localhost以外的任何IP地址;
- 执行shell命令、读写文件系统、修改环境变量。
第三层:网络级隔离
Docker部署时,用 --network=none 禁用网络,仅通过 --add-host 显式添加白名单域名:
docker run --network=none \
--add-host=api.jd.com:192.168.1.100 \
--add-host=smtp.company.com:192.168.1.200 \
price-monitor-agent
实战教训:某次测试中,Claude因提示词漏洞,生成了
curl http://169.254.169.254/latest/meta-data/(AWS元数据服务)的工具调用。幸好有网络隔离,请求直接超时,未造成信息泄露。
6. 运维与扩展:让Agent真正融入你的工作流
6.1 日志体系:不只是记录,而是可回溯的决策链
一个合格的Agent日志,必须能回答三个问题:
- 它当时看到了什么? (输入消息、工具参数)
- 它做了什么决定? (调用了哪个工具、为何调用)
- 结果如何影响后续? (工具返回值、Claude的下一步动作)
我的日志结构(JSON Lines格式):
{
"timestamp": "2024-10-22T09:00:01.234Z",
"step": 3,
"role": "assistant",
"content_type": "tool_use",
"tool_name": "get_price_data",
"tool_input": {"sku_id": "100012345678", "days": 7},
"decision_reason": "User requested price data for SKU 100012345678"
}
{
"timestamp": "2024-10-22T09:00:05.678Z",
"step": 3,
"role": "user",
"content_type": "tool_result",
"tool_use_id": "get_price_data_1729587601_4567",
"tool_result": {"status": "success", "data": {"current_price": 299.0, ...}},
"next_action": "call compare_prices"
}
用 jq 可快速分析:
- 查看所有失败调用:
jq 'select(.tool_result.status == "error")' agent.log - 统计各工具调用频次:
jq -r '.tool_name' agent.log | sort | uniq -c | sort -nr
提示:日志必须写入独立文件,而非stdout。Docker中用
docker logs -f price-monitor查看实时日志,比翻文件高效。
6.2 扩展性设计:如何低成本接入新工具(以“微信通知”为例)
当运营提出“价格波动时,除了邮件,还要发微信通知”,你不想重写整个Agent。我的扩展方法:
步骤1:定义新工具Schema
class SendWechatInput(BaseModel):
user_id: str = Field(..., description="企业微信用户ID,如zhangsan")
content: str = Field(..., description="通知内容,不超过2000字")
def send_wechat_schema():
return SendWechatInput.model_json_schema() | {"title": "send_wechat"}
步骤2:在 execute_tool 中添加分支
elif tool_name == "send_wechat":
return send_wechat(**tool_input) # 实现函数
步骤3:更新系统提示词,在 <tools> 中追加新工具定义
关键点 :无需修改主循环、不重启服务、不改动已有工具。只要Claude的提示词中声明了新工具,它就会自动规划调用。
实测:从接到需求到上线微信通知,共耗时22分钟(15分钟写代码,7分钟测试)。这才是Agent应有的敏捷性。
6.3 成本监控:如何避免Claude账单暴增
Claude按输入+输出token计费。一个未优化的Agent,单次任务可能消耗5万token(大部分浪费在重复的历史消息上)。
我的成本控制策略:
策略1:消息压缩
不在 messages 中保存全部历史,只保留:
- 最近2轮用户消息;
- 最近1轮工具调用及结果;
- 系统提示词(固定不变)。
策略2:输出截断
在 client.messages.create() 中,用 stop_sequences=["</final_answer>"] ,让Claude在生成完PDF Base64后立即停止,避免冗余描述。
策略3:缓存机制
对 get_price_data 结果,用Redis缓存2小时:
cache_key = f"price:{sku_id}:{days}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 否则调用API,存入redis
redis_client.setex(cache_key, 7200, json.dumps(result))
数据:优化后,单次任务token消耗从42,156降至3,842,成本降低91%,且响应更快。
我个人在实际操作中的体会是:Agent开发的终点,不是让模型“更聪明”,而是让整个系统“更可控”。当你能清晰说出每一次工具调用的必然性、每一次失败的可解释性、每一次输出的可验证性,你就已经超越了90%的所谓“AI工程师”。这个教程里没有玄学,只有我在机房里熬过的夜、改过的37版提示词、以及贴在显示器边上的那张手写故障树。现在,它属于你了。
更多推荐


所有评论(0)