如何构建 AI Agent:规划、工具使用与执行

如何构建 AI Agent:规划、工具使用与执行

构建 AI Agent 的起点是一个简单的想法:让模型能够执行动作、观察结果,并形成循环。模型针对目标进行推理,决定下一步做什么,调用某个工具,获取结果,然后继续执行,直到任务完成。这个“读取-评估-行动”循环,无论你构建的是单步工具调用器,还是能编写代码、运行并自我纠错的多步系统,都是其核心所在。

本教程将带你逐一了解构成 Agent 的四个组件:规划、工具使用、代码执行和评估。学完之后,你将建立起一个可运行的心智模型和一套极简实现方案。

什么构成了 AI Agent

AI Agent 与基础 LLM 调用的关键区别在于:它能够执行动作并观察其结果。

普通的聊天补全只生成文本。而 Agent 则利用这些文本来调用函数、查询 API、运行命令或读取文件——并将结果作为下一步的输入反馈回来。正是这个反馈循环,使得 Agent 能够处理那些无法通过单次响应完成的任务。

一个 Agent 具备三种核心能力:

  • 工具调用 — 通过结构化的参数按名称调用定义的函数
  • 上下文累积 — 随着对话增长,将工具执行结果持续带入
  • 目标导向的循环 — 持续运行直到满足停止条件,而非仅仅生成一次响应

如果某个系统同时具备这三者,那你就是在构建 Agent。如果缺少上下文累积或循环机制,那它只是一个工具增强型聊天机器人。

同样的架构可以处理广泛的任务:能够搜索并综合信息的研究型 Agent、能够编写并运行代码的编程型 Agent、能够查询数据库并生成报告的数据型 Agent。每种场景下的实现模式几乎完全相同。

第一步:规划 — 推理层

规划层是模型决定下一步行动的地方。你不需要一个独立的规划模型——生成文本的同一个模型,可以在每一步针对目标进行推理,并产生结构化的行动(即工具调用)。

系统提示词完成了大部分工作。一个优秀的规划提示词应告知模型:

  • 它正在努力实现什么目标
  • 哪些工具可用,以及每个工具的功能
  • 调用工具时应使用什么格式
  • 何时停止(完成条件)

使用兼容 OpenAI 的 Novita LLM API 时,示例如下:

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.novita.ai/v3/openai",
    api_key=os.environ["NOVITA_API_KEY"],
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "run_code",
            "description": "执行 Python 代码并返回 stdout/stderr。",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {
                        "type": "string",
                        "description": "要执行的 Python 代码"
                    }
                },
                "required": ["code"]
            }
        }
    }
]

messages = [
    {
        "role": "system",
        "content": "你是一个编程助手。使用 run_code 工具执行 Python 代码来解决问题。当你得到最终答案时,请停止。"
    },
    {
        "role": "user",
        "content": "找出 50 以下的所有质数。"
    }
]

response = client.chat.completions.create(
    model="meta-llama/llama-3.1-8b-instruct",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

模型返回的是一个工具调用——一个包含函数名和参数的 JSON 对象——而不是纯文本响应。你的代码需要提取这个对象,执行相应的函数,然后将结果附加到 messages 中并进入下一轮循环。

第二步:工具使用 — 将模型连接到世界

工具是 Agent 实际执行的动作。没有工具,模型只能生成文本;有了工具,它可以读取文件、调用 API、运行代码、搜索数据库或触发你定义的任何函数。

定义工具

每个工具都是一个描述函数的 JSON 模式。模型通过读取这个模式来正确地格式化调用请求。几条实用规则:

  • 描述要保持简短精确。 模型在每一步都会读取这些描述。冗长的描述只会浪费 token。
  • 参数名称要与函数实际接受的参数匹配。 模式与实现之间的不匹配会导致静默失败。
  • 返回结构化的输出。 如果你的工具返回原始 HTML 或未格式化的文本,模型会浪费 token 去解析它。应返回带有清晰标签的干净字符串或 JSON。

工具调用循环

模型发出工具调用后,你的代码需要:

  1. 从响应中解析出工具名称和参数
  2. 查找对应的 Python 函数
  3. 使用解析出的参数调用该函数
  4. 将结果以 role: "tool" 的形式附加到 messages
  5. 发起另一次补全请求

循环持续进行,直到模型返回一条不包含工具调用的普通消息(即完成条件)。

import json

def run_agent_loop(client, model, messages, tools, tool_fns, max_steps=10):
    for _ in range(max_steps):
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        choice = response.choices[0]

        # 完成 — 没有工具调用
        if choice.finish_reason == "stop":
            return choice.message.content

        # 执行工具调用
        tool_call = choice.message.tool_calls[0]
        fn_name = tool_call.function.name
        fn_args = json.loads(tool_call.function.arguments)

        result = tool_fns[fn_name](**fn_args)

        # 同时附加模型的工具调用及结果
        messages.append(choice.message)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": str(result),
        })

    return "已达到最大步数,未得到最终答案。"

这就是完整的 Agent 循环。max_steps 守卫防止了失控循环——如果模型陷入反复调用同一个工具的僵局,循环会终止而不是无限运行。其他所有功能——多工具编排、记忆、流式输出——都建立在这个模式之上。

常见工具模式

文件访问: 在 Agent 的工作目录内读写文件。将文件内容以字符串形式返回。

网络搜索:{title, url, snippet} 对象列表的形式返回搜索结果。摘要要简短。

代码执行: 在隔离环境中执行代码并返回 stdout/stderr(下一节会详细讨论)。

结构化数据查询: 查询数据库或 API,并以 JSON 形式返回记录。模型负责进行总结。

每个工具应只做一件事,并且在相同输入下返回确定性的输出。试图在单个工具中塞入太多逻辑的 Agent,会变得更难调试和评估。

第三步:代码执行 — 安全地运行代码

如果你的 Agent 会生成并运行代码,那么你需要一个隔离的环境。直接在机器上运行模型生成的代码存在安全风险:模型可能会产生破坏性的 shell 命令、无限循环,或者读取其不应访问的敏感文件。

标准的做法是使用 沙盒——一个短生命周期的、隔离的 Linux 环境,Agent 可以在其中自由执行代码,而不会影响宿主机系统。Novita 的 Agent Sandbox 正是为此场景设计的:一个基于云的执行环境,在 200ms 内启动,运行 Agent 生成的任何代码,并在会话结束时销毁该环境。

以下是在规划部分中,run_code 工具使用沙盒的实现方式:

from novita_sandbox.code_interpreter import Sandbox

sandbox = Sandbox.create()

def run_code(code: str) -> str:
    result = sandbox.run_code(code)
    output = ""
    if result.logs.stdout:
        output += "\n".join(result.logs.stdout)
    if result.logs.stderr:
        output += "\nSTDERR: " + "\n".join(result.logs.stderr)
    return output or "(无输出)"

当 Agent 调用 run_code 时,此函数在隔离环境中执行代码,并将 stdout/stderr 作为字符串返回。Agent 看到结果后,决定下一步做什么——无论是修复 bug、继续使用输出,还是宣告任务完成。

这种架构清晰地将推理层与执行层分离开来。LLM 负责规划和调用工具;沙盒负责运行代码,而不暴露容器之外的任何内容。

沙盒处理的内容

  • Shell 命令(bashgitpythonnode
  • 沙盒内的文件系统操作
  • 容器内部的网络请求
  • 通过异步结果检索进行的长时间运行的后台任务
  • 用于多会话工作流的暂停与恢复

沙盒不允许的操作包括:访问宿主机系统、其他沙盒,或你未明确传入的凭据。正是这种隔离性,使得让模型编写并运行任意代码变得安全。

第四步:评估 — 判断 Agent 是否正确

评估是 Agent 开发中最容易被跳过的一步。能够产生输出的 Agent,并不等同于能够产生正确输出的 Agent。

三个评估层级

1. 确定性检查

对于有已知正确答案的任务——如解析日期、计算数值、提取字段——可以针对 Agent 的输出编写单元测试。这类测试运行成本低,且能快速捕获回归问题。

2. 基于执行的检查

对于编程 Agent,最可靠的信号是:代码是否真正运行并产生了正确的结果?在沙盒中执行 Agent 的输出,并将 stdout 与期望值进行比较。这比比较代码文本更可靠,因为它测试的是行为,而非语法。

def evaluate_code_task(agent_code: str, expected_stdout: str) -> bool:
    result = run_code(agent_code)
    return result.strip() == expected_stdout.strip()

3. LLM 作为评判者

对于开放式任务——如撰写摘要、回答研究问题——使用第二个 LLM 调用,根据评分标准对输出进行评分。评分标准要具体:“答案是否包含一个可运行的代码示例?” 比 “答案好不好?” 更有效。模糊的评分标准会产生不一致的分数。

追踪失败模式

Agent 的失败通常表现为以下几种重复模式:

失败类型 原因 修复方法
工具调用循环 模型反复调用同一个工具 设置 max_steps;记录中间步骤
参数不匹配 模式与实现不一致 在调用前验证工具参数
上下文溢出 会话超出模型的上下文限制 对较早的工具结果进行总结或截断
捏造的工具名称 模型虚构了一个不存在的工具 使用严格的工具列表,配合 tool_choice: "auto"

评估不是开发末尾的一个独立阶段——它本身就是开发的一部分。每次扩展 Agent 的能力时,至少要为新行为编写一个能够捕获回归的测试。

为 Agent 推理选择模型

大多数教程在涉及 Agent 推理时,默认使用闭源模型。这在原型开发阶段效果不错,但在生产环境中——一个 Agent 可能每个任务需要 20 次 LLM 调用——模型成本和速率限制会成为真正的约束。

通过 Novita LLM API 托管的开源模型,在 Agent 实际需要的任务上——函数调用、结构化输出、多步推理——正逐渐匹配闭源模型的性能。像 Llama 3.3 70BDeepSeek-V3 这样的模型,在编码和指令遵循方面达到了 GPT-4 级别的水平,而运行成本却只有几分之一。对于高吞吐量的 Agent 工作负载,这种差异会迅速累积。

Novita API 完全兼容 OpenAI,因此从闭源模型切换到开源模型只需一行代码:

# 之前
model = "gpt-4o"

# 之后 — 相同的 API 契约,更低的成本
model = "meta-llama/llama-3.3-70b-instruct"

对于对延迟敏感的 Agent 循环,像 meta-llama/llama-3.1-8b-instruct 这样的小模型响应更快,每一步的成本也更低,这在每个用户任务涉及多次顺序调用时尤为重要。一个实用的模式是:在路由和分类步骤中使用更快、更小的模型,而在需要更多能力的复杂推理步骤中使用更大的模型。

Novita 的模型库还为特定的 Agent 子任务提供了强大的选项——用于处理基于图像的工具结果的视觉模型、用于检索的嵌入模型,以及用于编程 Agent 的代码专用模型——所有这些都可以通过同一个 API 端点访问。

整合在一起 — 一个最小可用 Agent

下面是一个完整的 Agent:它接收一个数学问题,生成 Python 代码来求解,在沙盒中运行代码,并返回验证后的结果:

import os
import json
from openai import OpenAI
from novita_sandbox.code_interpreter import Sandbox

client = OpenAI(
    base_url="https://api.novita.ai/v3/openai",
    api_key=os.environ["NOVITA_API_KEY"],
)
sandbox = Sandbox.create()

def run_code(code: str) -> str:
    result = sandbox.run_code(code)
    stdout = "\n".join(result.logs.stdout)
    stderr = "\n".join(result.logs.stderr)
    return stdout or stderr or "(无输出)"

tool_fns = {"run_code": run_code}

tools = [
    {
        "type": "function",
        "function": {
            "name": "run_code",
            "description": "执行 Python 代码。返回 stdout 或 stderr。",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string"}
                },
                "required": ["code"]
            }
        }
    }
]

messages = [
    {
        "role": "system",
        "content": "通过编写并运行 Python 代码来解决问题。使用 run_code 来执行代码。当你得到验证后的答案时,请停止。"
    },
    {
        "role": "user",
        "content": "求 1,000,000 以下所有偶数斐波那契数的和。"
    }
]

for _ in range(10):
    response = client.chat.completions.create(
        model="meta-llama/llama-3.1-8b-instruct",
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )
    choice = response.choices[0]

    if choice.finish_reason == "stop":
        print(choice.message.content)
        break

    tool_call = choice.message.tool_calls[0]
    fn_args = json.loads(tool_call.function.arguments)
    result = tool_fns[tool_call.function.name](**fn_args)

    messages.append(choice.message)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": result,
    })

sandbox.kill()

这段代码用大约 50 行涵盖了完整的 Agent 循环:规划、工具调用、执行、上下文累积、最大步数限制,以及干净的沙盒清理。Agent 编写代码、运行代码、查看输出,然后要么继续执行,要么给出最终答案。

若要扩展它:在 tool_fnstools 中添加更多工具,为更复杂的任务换用更大的模型,或者将沙盒的 run_code 替换为任何其他函数——网络搜索、数据库查询、API 调用。循环结构保持不变。

常见问题

AI Agent 和聊天机器人有什么区别?

聊天机器人生成文本回复。而 Agent 则执行动作——调用工具、运行代码、查询 API——并利用结果来继续朝着目标推理。是否存在工具调用和反馈循环,是区分 Agent 的关键。

构建 AI Agent 是否需要 LangChain 这样的框架?

不需要。核心的 Agent 循环足够简单,可以直接对着任何兼容 OpenAI 的 API 实现。框架在复杂的编排场景中(多 Agent 系统、持久化记忆、流式输出)有它的价值,但它们增加了抽象层,可能使调试变得更困难。先从原始 API 开始,如果你确实需要框架提供的功能,再考虑引入。

如何防止 Agent 运行危险代码?

使用沙盒化的执行环境。Agent 的代码在一个隔离的容器中运行,该容器无法访问你的宿主机系统或其他进程。沙盒在会话结束后会被销毁,因此 Agent 所做的任何更改都仅限于该容器。

开源模型能可靠地处理函数调用吗?

可以——但需注意模型规模。Llama 3.3 70B、DeepSeek-V3 和 Qwen 2.5 72B 都能很好地处理结构化的工具调用。较小的模型(7B–8B)适用于简单、定义清晰的工具模式,但在面对模糊指令或复杂的多步推理链时可能会遇到困难。对于生产级 Agent,在确定使用某个模型之前,请在实际的任务分布上进行测试。

Agent 每个任务应该进行多少步?

设置一个硬性上限——对于大多数任务,10 到 20 步是典型值。记录中间步骤,这样你可以清楚地看到 Agent 做了什么,并调试失败原因。持续需要更多步骤的任务,通常更适合拆分为子 Agent 或子任务,而不是简单地去扩展循环上限。

推荐阅读