核心机制:模型从不执行代码

新手常误以为存在某种黑盒机制,但事实更为简单:模型本身绝不运行任何代码。开发者需提供一份允许调用的函数列表。当模型判定需要调用某函数时,它会暂停文本生成,输出一段包含函数名及JSON参数的结构化数据。宿主程序读取该数据,执行本地函数,并将返回值重新注入对话上下文,随后再次请求模型。

这一过程构成一个四步循环。图中不存在独立的记忆或规划模块。对话数组即为状态,循环本身即为程序。无工具的模型仅是写作者;结合此循环与工具的模型,才构成智能体(Agent)。

模型交互细节

开发者只需在JSON Schema中定义工具名称、描述及参数结构。模型像处理其他文本一样阅读这些描述,并生成符合结构的参数。这带来两点关键启示:

  • 必须解析JSON:参数以JSON格式返回,严禁通过字符串匹配处理。
  • 描述即提示词:若模型频繁误用工具,通常源于描述不清。编写模型可准确理解的描述是进阶关键。

底层逻辑无需依赖框架。尽管后续课程介绍的框架会自动运行此循环,但其核心依然是上述四个步骤。

代码实现范例

以下提供基于Anthropic SDK的完整可运行代码。需安装相应包并配置ANTHROPIC_API_KEY

JavaScript 实现

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

// 1. 工具:普通函数,无特殊之处
function getWeather({ city }) {
  const readings = { Paris: "18°C, light rain", Tokyo: "27°C, clear" };
  return readings[city] ?? `no reading for ${city}`;
}

// 2. 描述:模型阅读的提示词
const tools = [{
  name: "get_weather",
  description: "Current weather for one city. Use this for any question " +
               "about temperature, rain, or conditions right now.",
  input_schema: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name, e.g. Paris" },
    },
    required: ["city"],
  },
}];

// 3. 循环:智能体的核心逻辑
const messages = [
  { role: "user", content: "Do I need an umbrella in Paris?" },
];

while (true) {
  const reply = await client.messages.create({
    model: "claude-opus-5",
    max_tokens: 4096,
    tools,
    messages,
  });

  // 原样追加回复,保留所有块信息
  messages.push({ role: "assistant", content: reply.content });

  // 若无需工具,则输出文本并结束
  if (reply.stop_reason !== "tool_use") {
    console.log(reply.content.filter(b => b.type === "text")
                             .map(b => b.text).join(""));
    break;
  }

  // 处理工具请求并回传结果
  const results = reply.content
    .filter(b => b.type === "tool_use")
    .map(b => ({
      type: "tool_result",
      tool_use_id: b.id, // ID是契约核心
      content: String(getWeather(b.input)),
    }));

  messages.push({ role: "user", content: results });
}

注意:工具结果通过role: "user"返回。虽看似怪异,但这符合协议规定:模型回合后紧跟回应回合。

Python 实现

import anthropic

client = anthropic.Anthropic();

# 1. 工具
def get_weather(city):
    readings = {"Paris": "18°C, light rain", "Tokyo": "27°C, clear"}
    return readings.get(city, f"no reading for {city}")

# 2. 描述
tools = [{
    "name": "get_weather",
    "description": "Current weather for one city. Use this for any question "
                   "about temperature, rain, or conditions right now.",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name, e.g. Paris"},
        },
        "required": ["city"],
    },
}]

# 3. 循环
messages = [
    {"role": "user", "content": "Do I need an umbrella in Paris?"},
]

while True:
    reply = client.messages.create(
        model="claude-opus-5",
        max_tokens: 4096,
        tools=tools,
        messages=messages,
    )

    messages.append({"role": "assistant", "content": reply.content})

    if reply.stop_reason != "tool_use":
        print("".join(b.text for b in reply.content if b.type == "text"))
        break

    results = [
        {
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": str(get_weather(**block.input)),
        }
        for block in reply.content if block.type == "tool_use"
    ]

    messages.append({"role": "user", "content": results})

其中get_weather(**block.input)对应JS中的解构赋值,将Schema属性名直接映射为函数参数。

对话形态与记忆机制

每轮交互都在数组中新增条目。模型本身无记忆,数组即记忆。十步长的智能体在最后一次请求时携带全部十个步骤的历史。这也解释了为何上下文长度直接影响费用。

成本核算与优化

每次请求均需发送完整对话数组。假设五次请求的输入令牌分别为0.3k、0.9k、2.0k、2.9k和4.2k,总计10.3k输入令牌,而最终对话长度仅为4.2k。用户需为构建内容的2.5倍大小付费。以Claude Opus 5每百万输入令牌5美元计,此例成本约0.05美元

若工具返回数据臃肿,成本将激增。例如,第二步返回8k令牌的文件,将在后续八次请求中重复发送,产生+64k输入令牌(约0.32美元)。若每日运行千次,仅因未修剪read_file结果,日成本可达320美元

测试环境中默认最大步数40,旨在限制此类计费上限。优化策略包括:

修剪工具结果,而非提示词。提示词仅发送一次,而工具结果会在后续每轮重复发送。
  • 减少返回量:返回行范围、计数或部分匹配项,而非全量数据。
  • 缓存前缀:使用cache_control可使重复前缀计费率降至输入率的0.1倍(首次写入收取1.25倍溢价)。

四条最佳实践

  • 回应所有调用:即使函数抛出异常,也应返回含is_error: truetool_result。缺失结果属协议违规,错误信息则有助于模型重试。
  • 合并返回结果:若模型并行请求多个工具,应在单个用户消息中返回所有tool_result。拆分消息会抑制模型的并行请求能力,降低效率。
  • 整体追加回复:直接推送reply.content,避免重建字符串。丢弃非文本块会导致下一轮质量下降且难以排查。
  • 限制循环次数:生产环境中应设置计数器(如20次),防止因模型误解描述而无限调用,避免高额账单。

权衡:手动循环 vs SDK运行器

掌握底层逻辑后,建议使用SDK提供的自动化工具。Python中使用@beta_tool装饰器的client.beta.messages.tool_runner(),TypeScript中使用betaZodToolclient.beta.messages.toolRunner()。这可大幅简化代码并自动应用上述最佳实践。

代价在于失去对控制流的细粒度掌控。如需在回合间插入审批、日志或重试逻辑,须通过钩子(hooks)实现,而非直接修改循环体。建议初学者先手动实现一次循环,以深入理解机制。

主流技术栈均遵循同一逻辑:

  • Python/TS:Anthropic官方SDK提供tool_runner
  • Vercel AI SDK:封装为generateText({ tools })
  • 其他语言:Go、Java等官方SDK均含工具运行器入口。
  • 本地模型:Ollama和llama.cpp支持OpenAI风格tools,逻辑一致。
  • MCP:作为获取工具的方式,由服务器填充tools数组,不改变循环本质。

练习与安全说明

建议在测试环境中实践:连接虚拟文件系统,执行写入与读取操作,观察tool_usetool_result的交替过程。尝试禁用工具或拼错ID,观察模型反应及错误处理。

测试环境仅在浏览器localStorage中操作虚拟文件,不涉及真实磁盘。/wipe可清除数据,/undo可恢复。唯一真实成本为令牌费,极其低廉,适合大胆试错。

下周预告

后续课程将深入探讨如何编写精准的工具描述,并在第3课中拆解循环与目标设定,解决模型永不停止调用工具等极端情况。从“描写天气”到“检查天气”,二十行代码与四个站点构成了智能体的基石。真正的技艺在于选择哪些工具值得加入该数组。