1. 项目概述:用50行Python解锁Anthropic的Advisor工具

最近在折腾AI应用开发,特别是想给一些内部系统加上智能问答和决策支持的能力。市面上大模型API很多,但很多方案要么太“重”,需要一整套复杂的工程架构;要么太“泛”,回答虽然流畅但缺乏针对具体业务场景的深度。直到我试用了Anthropic的Claude API,尤其是他们提供的 Advisor工具 ,发现这玩意儿简直是给垂直领域应用“开外挂”的神器。它不像普通的聊天补全,你扔一个问题它给一个答案就完了。Advisor更像一个内置了“思考链”和“工具箱”的智能体,能根据你的问题,自动规划步骤、调用你预先定义好的函数(工具)去获取信息,然后给出经过推理和验证的结论。

最让我惊喜的是,它的接入成本低得离谱。官方文档可能看起来有点复杂,但经过一番摸索,我发现 核心的调用逻辑,用大约50行清晰、可维护的Python代码就能完整实现 。这意味着一线业务人员、数据分析师,甚至是对后端不太熟悉的前端同学,都能快速上手,把强大的推理能力嵌入到自己的脚本、自动化流程或者轻量级Web应用里。今天我就来拆解一下这50行代码背后的门道,从环境准备、核心对象解析,到完整的请求-响应循环,最后再分享几个我踩过坑才总结出来的实战技巧。

2. 核心思路与架构拆解

2.1 什么是Advisor工具?它解决了什么痛点?

在深入代码之前,我们得先搞清楚Advisor工具到底是什么,以及为什么它值得我们用代码去集成。你可以把它理解为Claude模型的一个“增强模式”。当我们使用标准的 messages API时,交互模式是线性的:用户提问,模型回答。但如果问题复杂,需要查数据、做计算或者分步骤推理,用户就得手动拆解问题,多次提问,或者提前把所有上下文喂给模型。

Advisor工具改变了这个范式。它允许开发者 预先定义一系列“工具” (本质上就是函数,描述了名称、作用、输入参数)。当你向Advisor提出一个复杂问题时,模型不会直接生成最终答案,而是先“思考”:要解决这个问题,我需要按什么步骤来?每一步需要调用哪个工具?需要传入什么参数?然后,它会以结构化的格式(一个 tool_use 块)告诉你它想调用哪个工具以及参数是什么。你的代码接收到这个请求后,就去 本地执行对应的真实函数 ,获取结果(比如查询了数据库、计算了指标、调用了某个外部API),再将结果以 tool_result 块的形式返回给模型。模型基于这个结果继续它的推理,可能再次调用工具,如此循环,直到它认为已经收集到足够的信息,可以生成最终答案。

这个过程解决了几个关键痛点:

  1. 信息实时性 :模型的知识有截止日期,但通过工具,它可以获取到实时、动态的数据(如当前股价、天气、库存)。
  2. 准确性 :涉及具体计算或业务逻辑时,让模型调用你写的、经过验证的函数,远比让它自己“心算”或“编造”要可靠得多。
  3. 可控性与安全性 :模型只能在你允许的“工具箱”范围内操作。它无法直接访问你的数据库或内部系统,必须通过你定义的、带有权限控制和输入校验的函数来间接访问。
  4. 复杂任务自动化 :将多步骤的推理和操作自动化,用户只需提出最终目标,剩下的规划、执行、整合由Advisor协同你的代码来完成。

2.2 50行代码的架构设计

要实现Advisor调用,核心是构建一个能与Claude API进行多轮“对话”的循环。这个循环的每一次迭代,都可能包含模型的“思考”(想调用工具)和我们的“执行”(运行工具并返回结果)。我们的50行代码,就是精心设计了这个循环的骨架。

整个架构围绕以下几个核心对象展开:

  • client (Anthropic客户端) :负责所有与Anthropic API服务器的网络通信。
  • tools (工具列表) :一个列表,里面每个元素都是一个字典,严格遵循Anthropic的 tool 格式,描述了工具的名字、描述和输入参数模式。这是Advisor的“工具箱说明书”。
  • messages (消息列表) :记录整个对话的历史。它不仅仅包含用户和AI的文本,还穿插着 tool_use (AI想用工具)和 tool_result (我们返回工具结果)这两种特殊的消息块。这个列表维护了完整的对话上下文。
  • real_tools (真实工具映射) :一个Python字典,键是工具名( name ),值是对应的、真正可以被执行的Python函数。这是“工具箱”本身。

代码的主循环逻辑如下:

  1. 初始化 :准备好 client ,定义好 tools real_tools ,初始化一个空的 messages 列表,并把用户的问题作为第一条用户消息加进去。
  2. 发起请求 :调用 client.messages.create() ,传入 model (如 claude-3-5-sonnet-20241022 )、 messages tools ,并 最关键的是设置 tool_choice={"type": "any"} ,这告诉API:“请让模型以Advisor模式运行,可以自主选择使用任何工具”。
  3. 处理响应 :API返回一个响应对象。我们遍历响应中的所有 content 块。
    • 如果块类型是 text ,说明模型输出了文本,我们把它追加到 messages 中,并可能直接展示给用户(如果是最终答案)。
    • 如果块类型是 tool_use ,说明模型想调用工具了!我们会从中提取 tool_name input (参数)。
  4. 执行工具 :根据 tool_name real_tools 字典里找到对应的真实函数,把 input 作为参数传进去执行。
  5. 返回结果 :将工具执行的结果(或错误信息)包装成一个 tool_result 块,追加到 messages 列表中。这个块必须通过 tool_use_id 关联到之前那个 tool_use 块。
  6. 循环 :由于 messages 列表末尾现在有了新的 tool_result ,循环回到第2步,再次调用 client.messages.create() 。模型会基于这个新结果继续推理,可能输出更多文本或再次调用工具。
  7. 终止 :当模型的响应中不再包含 tool_use 块,且输出了完整的 text 内容时,循环结束,我们得到了最终答案。

这个设计巧妙地将AI的“规划”能力与我们本地代码的“执行”能力结合了起来,而代码本身主要扮演一个“调度员”和“记录员”的角色。

3. 环境准备与核心依赖解析

3.1 安装与配置Anthropic Python SDK

一切开始之前,你得有一个能访问Claude API的环境。首先,确保你有一个Anthropic的账户,并在其控制台创建了API密钥。这个密钥是你的通行证,务必妥善保管,不要硬编码在脚本里。

接下来是Python环境。我强烈建议使用虚拟环境( venv conda )来管理依赖,避免污染全局环境。

# 创建并激活虚拟环境(以venv为例)
python -m venv claude-advisor-env
source claude-advisor-env/bin/activate  # Linux/macOS
# claude-advisor-env\Scripts\activate  # Windows

# 安装官方Anthropic SDK
pip install anthropic

anthropic 这个库就是我们的核心武器。它封装了所有与API交互的细节,让我们能用非常Pythonic的方式调用Claude。目前,确保你安装的是较新的版本(如 >=0.25.0 ),以支持完整的工具调用功能。

3.2 API密钥的安全管理

把API密钥直接写在代码里是绝对的大忌。一旦代码上传到GitHub或其他地方,密钥就泄露了,会带来严重的财务和安全风险。正确的做法是使用环境变量。

# 在终端中设置环境变量(临时,重启后失效)
export ANTHROPIC_API_KEY='your-api-key-here'

# 或者,更持久的方法,写入shell配置文件(如 ~/.bashrc 或 ~/.zshrc)
echo "export ANTHROPIC_API_KEY='your-api-key-here'" >> ~/.zshrc
source ~/.zshrc

在Python代码中,通过 os 模块来读取:

import os
from anthropic import Anthropic

api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
    raise ValueError("请设置 ANTHROPIC_API_KEY 环境变量。")

client = Anthropic(api_key=api_key)

对于更复杂的项目,可以考虑使用 .env 文件配合 python-dotenv 库,或者在云服务平台使用其秘密管理服务。

3.3 工具定义:连接AI与真实世界的桥梁

tools 参数是我们代码中第一个需要精心构造的部分。它不是一个可以随意写的Python字典,而必须遵循严格的JSON Schema格式。这就像是给AI的一份“工具使用说明书”,必须清晰、无歧义。

一个完整的工具定义通常包含以下字段:

  • name : 工具的唯一标识符,字符串。最好使用蛇形命名法( snake_case ),清晰易懂,如 get_current_weather
  • description : 对工具功能的自然语言描述。 这个描述至关重要! AI主要靠它来理解何时以及如何使用这个工具。描述应简洁,说明输入是什么,输出是什么。例如:“获取指定城市的当前天气情况。”
  • input_schema : 一个符合JSON Schema的字典,严格定义工具所需的参数。必须包含 type: "object" properties 字段。每个属性都要定义其 type (如 string , number )、 description (参数描述)等信息。

下面是一个定义“查询天气”和“执行计算”工具的示例:

tools = [
    {
        "name": "get_weather",
        "description": "根据城市名称查询当前的天气状况,包括温度、天气现象和湿度。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city_name": {
                    "type": "string",
                    "description": "要查询天气的城市名称,例如:北京、上海、New York。"
                }
            },
            "required": ["city_name"]  # 指定哪些参数是必须的
        }
    },
    {
        "name": "calculator",
        "description": "执行一个简单的数学计算。支持加(+)、减(-)、乘(*)、除(/)。",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "数学表达式,例如:'3 + 5 * (2 - 1)'。"
                }
            },
            "required": ["expression"]
        }
    }
]

注意 input_schema 中的 description 字段对于AI理解参数意图非常有帮助。尽量写清楚,比如“城市名称”就比“location”更明确。同时, required 列表可以防止AI漏掉关键参数。

4. 核心代码逐行详解与实现

现在,我们把架构中的各个部分用代码组装起来。我会将完整的50行左右代码分块解释,你可以把它们拼接成一个完整的脚本。

4.1 初始化与工具定义

import os
from anthropic import Anthropic
import json

# 1. 初始化客户端
api_key = os.environ.get("ANTHROPIC_API_KEY")
client = Anthropic(api_key=api_key)

# 2. 定义给AI看的工具列表 (Tools for AI)
tools_for_ai = [
    {
        "name": "get_weather",
        "description": "获取指定城市的当前天气信息。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名称,例如:北京、东京、San Francisco"}
            },
            "required": ["city"]
        }
    },
    {
        "name": "calculate",
        "description": "计算一个数学表达式的结果。",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {"type": "string", "description": "数学表达式,如:'2 + 3 * 4' 或 '(10 - 5) / 2'"}
            },
            "required": ["expression"]
        }
    }
]

# 3. 定义真实的工具函数映射 (Real Tools Implementation)
def real_get_weather(city: str) -> str:
    """模拟天气查询函数。在实际应用中,这里会调用天气API。"""
    # 这里只是一个模拟。真实情况可能调用如OpenWeatherMap的API。
    weather_data = {
        "北京": "晴朗,25°C,湿度40%",
        "上海": "多云,28°C,湿度65%",
        "San Francisco": "雾,18°C,湿度80%"
    }
    return weather_data.get(city, f"未找到城市 {city} 的天气信息。")

def real_calculate(expression: str) -> str:
    """安全地计算数学表达式。"""
    try:
        # 警告:在生产环境中,直接使用eval是危险的,可能造成代码注入。
        # 这里仅作演示。安全做法是使用ast.literal_eval或专门的数学表达式解析库。
        result = eval(expression)
        return f"表达式 `{expression}` 的计算结果是:{result}"
    except Exception as e:
        return f"计算表达式 `{expression}` 时出错:{e}"

# 将工具名映射到真实的函数
real_tools = {
    "get_weather": real_get_weather,
    "calculate": real_calculate
}

这部分代码做了三件事:

  1. 创建了API客户端。
  2. 定义了 tools_for_ai ,这是给Claude看的“工具菜单”。
  3. 定义了 real_tools 字典和对应的函数 real_get_weather real_calculate 。这是我们的真实“工具库”。 注意两个 calculate 函数中的安全警告 ,演示中用了 eval ,但实际产品中必须替换为更安全的方案。

4.2 主循环:与Advisor的对话引擎

这是最核心的部分,实现了之前描述的请求-响应循环。

def run_advisor_cycle(user_query: str, max_turns: int = 10):
    """
    运行Advisor对话循环。
    :param user_query: 用户的初始问题
    :param max_turns: 最大对话轮次,防止无限循环
    """
    # 初始化消息历史,加入用户问题
    messages = [{"role": "user", "content": user_query}]

    for turn in range(max_turns):
        print(f"\n--- 第 {turn + 1} 轮对话 ---")

        # 1. 调用Claude API,关键是指定 tool_choice="any"
        response = client.messages.create(
            model="claude-3-5-sonnet-20241022", # 使用支持工具调用的模型
            max_tokens=1024,
            messages=messages,
            tools=tools_for_ai,
            tool_choice={"type": "any"}  # 允许模型自主选择使用工具
        )

        # 2. 处理响应内容
        new_messages_content = []  # 用于收集本轮要追加的消息块
        should_continue = False    # 标记是否需要继续循环(即是否有工具调用)

        for content_block in response.content:
            if content_block.type == 'text':
                # 模型输出了文本
                print(f"Claude: {content_block.text}")
                new_messages_content.append({"type": "text", "text": content_block.text})
            elif content_block.type == 'tool_use':
                # 模型想要使用工具!
                tool_name = content_block.name
                tool_input = content_block.input
                tool_use_id = content_block.id

                print(f"Claude 想调用工具 `{tool_name}`,参数:{tool_input}")

                # 3. 执行对应的真实工具
                tool_func = real_tools.get(tool_name)
                if tool_func:
                    try:
                        # 调用函数,传入参数
                        tool_output = tool_func(**tool_input)
                        print(f"工具 `{tool_name}` 执行结果:{tool_output}")
                    except Exception as e:
                        tool_output = f"工具执行出错:{e}"
                        print(f"工具 `{tool_name}` 执行失败:{e}")
                else:
                    tool_output = f"错误:未找到名为 `{tool_name}` 的工具实现。"
                    print(tool_output)

                # 4. 将工具执行结果构建成 tool_result 块
                # 注意:必须使用正确的 tool_use_id 来关联
                new_messages_content.append({
                    "type": "tool_result",
                    "tool_use_id": tool_use_id,
                    "content": tool_output
                })
                should_continue = True  # 因为有工具调用,需要继续循环

        # 将本轮产生的所有新内容块(text 和 tool_result)作为一个整体追加到历史中
        if new_messages_content:
            messages.append({"role": "assistant", "content": new_messages_content})

        # 5. 判断循环是否结束
        if not should_continue:
            print("\n--- 对话结束,已获得最终答案 ---")
            break
    else:
        # 如果for循环正常结束(未break),说明达到了最大轮次
        print(f"\n警告:已达到最大对话轮次({max_turns}),强制结束。")

让我们拆解这个循环:

  • max_turns :一个安全阀,防止AI陷入无限的工具调用循环。
  • response = client.messages.create(...) :这是核心调用。 tool_choice={"type": "any"} 是激活Advisor模式的关键。你也可以用 {"type": "tool", "name": "specific_tool"} 来强制使用某个工具,但 “any” 模式才能体现其自主规划能力。
  • 遍历 response.content :响应内容是一个列表,可能包含多个块( TextBlock ToolUseBlock )。我们必须按顺序处理。
  • tool_use 块的处理:这是枢纽。我们提取 name input ,然后用 real_tools[tool_name](**tool_input) 来调用真实函数。 **tool_input 是将字典参数解包传递给函数的关键技巧。
  • 构建 tool_result :这是将现实世界的数据反馈给AI的桥梁。 tool_use_id 必须与触发它的那个 tool_use 块的 id 一致 ,否则API无法正确关联。
  • 循环条件 should_continue :如果本轮响应中有 tool_use ,我们就设置 should_continue=True ,让循环继续。如果只有 text ,说明AI给出了最终答案,循环结束。

4.3 完整示例与调用

将以上两部分代码组合,并添加一个入口点,就是一个完整的可运行脚本:

if __name__ == "__main__":
    # 示例问题:一个需要多步推理和工具调用的问题
    query = "请问北京现在的天气怎么样?如果天气晴朗,帮我计算一下(25 + 17)乘以2等于多少?"
    print(f"用户问题:{query}")
    run_advisor_cycle(query)

运行这个脚本,你将会在控制台看到类似以下的对话流:

用户问题:请问北京现在的天气怎么样?如果天气晴朗,帮我计算一下(25 + 17)乘以2等于多少?
--- 第 1 轮对话 ---
Claude 想调用工具 `get_weather`,参数:{'city': '北京'}
工具 `get_weather` 执行结果:晴朗,25°C,湿度40%
--- 第 2 轮对话 ---
Claude: 北京现在的天气是晴朗,25°C,湿度40%。
Claude 想调用工具 `calculate`,参数:{'expression': '(25 + 17) * 2'}
工具 `calculate` 执行结果:表达式 `(25 + 17) * 2` 的计算结果是:84
--- 第 3 轮对话 ---
Claude: 根据计算,(25 + 17) 乘以 2 等于 84。
--- 对话结束,已获得最终答案 ---

可以看到,Advisor自动将复杂问题拆解成了“先查天气,再根据结果决定是否计算”的步骤,并依次调用了我们定义的工具,最终整合信息给出了连贯的回答。

5. 高级技巧与实战避坑指南

掌握了基础实现后,下面这些从实战中总结的经验,能让你用得更顺手、更稳健。

5.1 工具设计的艺术:如何写出AI爱用的工具

工具定义的好坏,直接决定了Advisor使用的流畅度和准确性。

  1. 描述要具体,职责要单一 description 字段避免模糊。对比“处理数据”和“计算给定列表的平均值”,后者清晰得多。一个工具最好只做一件事(单一职责原则),这样AI更容易理解和调用。
  2. 参数设计要友好 input_schema 中的参数名应直观。使用 city_name 而非 loc 。为每个参数提供清晰的 description 。合理使用 required 字段,确保AI不会遗漏关键信息。
  3. 处理枚举和约束 :如果参数只能是几个特定值,可以在 schema 中使用 enum 进行约束。例如,一个 currency_converter 工具, from_currency to_currency 可以限定为 ["USD", "CNY", "EUR", "JPY"] 。这能极大提高AI调用时的准确性。
    "input_schema": {
        "type": "object",
        "properties": {
            "from_currency": {"type": "string", "enum": ["USD", "CNY", "EUR", "JPY"]},
            "to_currency": {"type": "string", "enum": ["USD", "CNY", "EUR", "JPY"]},
            "amount": {"type": "number"}
        },
        "required": ["from_currency", "to_currency", "amount"]
    }
    
  4. 提供示例 :在工具描述的末尾,可以加一句“例如: get_stock_price('AAPL') ”。这能给AI很强的提示。

5.2 错误处理与鲁棒性增强

我们的基础代码已经有了简单的 try...except ,但在生产环境中需要更周全。

  1. 工具函数内部的健壮性 :真实工具函数(如 real_get_weather )必须包含完善的错误处理。网络请求超时、API返回异常格式、数据库查询失败等情况都要考虑,并返回清晰的错误信息给AI,例如:“天气服务暂时不可用,请稍后再试。”而不是抛出一个Python异常堆栈。
  2. 处理未知工具 :虽然我们定义了映射,但要预防AI因为上下文混淆或“幻觉”而请求一个未定义的工具。在 tool_func = real_tools.get(tool_name) 后,如果 tool_func None ,应该返回一个友好的错误信息,如:“抱歉,我目前无法执行‘XXX’这个操作。”
  3. 参数校验前置 :可以在调用真实函数前,先根据 input_schema 对AI传入的 tool_input 做一次校验。比如检查必填字段是否存在,类型是否匹配,枚举值是否有效。这可以提前拦截很多问题。
  4. 设置超时与重试 :对于网络调用类工具,务必设置超时(如使用 requests 库的 timeout 参数),并考虑加入简单的重试逻辑(如重试2次),避免一次网络波动导致整个对话卡住。

5.3 性能优化与成本控制

频繁调用API会产生费用,也需要时间。

  1. 缓存工具结果 :对于一些耗时或调用成本高、且结果在短时间内不变的工具(如天气查询、股票价格),可以引入简单的缓存机制。例如,用 city 作为键,将结果在内存中缓存5分钟。
    from functools import lru_cache
    import time
    
    @lru_cache(maxsize=128)
    def real_get_weather_with_cache(city: str) -> str:
        # ... 真实的API调用 ...
        pass
    
    注意:需要根据业务场景决定缓存策略,对于实时性要求高的数据不能缓存。
  2. 精简上下文(Messages) messages 列表会随着对话轮次增长,每次都会发送给API。Token消耗与成本直接相关。对于非常长的多轮对话,可以考虑在适当的时候(比如一个问题完全解决后)清空或截断历史,或者使用API可能提供的“上下文摘要”功能(如果未来支持)。
  3. 选择合适的模型 claude-3-haiku 模型更快、更便宜,对于工具调用这类结构化任务,其能力可能已经足够。 claude-3-5-sonnet 能力更强但更贵。根据任务复杂度进行选择。
  4. 限制最大轮次( max_turns :我们代码里已经做了,这是防止意外情况导致循环失控、产生高额费用的必要措施。一般设置为5-10轮已经能处理绝大多数复杂任务。

5.4 扩展应用场景

这50行代码是一个强大的框架,可以轻松扩展到各种场景:

  1. 企业内部知识库问答 :定义 search_internal_wiki 工具,函数内部连接你的Elasticsearch或向量数据库,Advisor就能根据员工提问,自动搜索并总结内部文档。
  2. 数据分析与报表 :定义 query_database generate_chart 工具。用户可以说:“帮我分析一下上季度华东区的销售数据,找出Top 3的产品,并总结趋势。”Advisor会自动规划查询、分析、可视化的步骤。
  3. 自动化工作流 :定义 send_email create_jira_ticket query_crm 等工具。Advisor可以理解“通知客户A项目延期,并在CRM中更新状态”这样的指令,并自动执行一系列操作。
  4. 智能客服升级 :当标准客服机器人无法解决时,可以无缝切换到Advisor模式,Advisor通过调用“查询订单”、“检查物流”、“发起退款”等工具,提供更深度的、可执行的服务。

关键在于,你将复杂的业务逻辑封装成一个个简单的工具函数,而Advisor则扮演了理解用户意图、并智能编排这些工具的大脑。这种“AI规划 + 本地执行”的模式,在安全可控的前提下,极大地扩展了AI的应用边界。

更多推荐