文章

Hello-Agents:从大语言模型到可执行的智能体系统

解释大语言模型如何通过行动闭环、工具协议、状态管理和上下文工程变成可执行、可控制的智能体系统。

Hello-Agents:从大语言模型到可执行的智能体系统

核心

大语言模型负责提出动作,确定性程序负责约束和执行动作;两者通过“观察—行动—反馈”形成闭环,才构成能够完成任务的 Agent。 因此,Agent 的可靠性不只取决于模型有多聪明,更取决于工具协议、状态管理、权限校验、结果验证和停止条件是否完整。

  1. 核心
  2. 从会回答问题到能完成任务
  3. Agent 是一个受控的行动闭环
  4. 大语言模型只负责提出下一步
  5. Function Calling 把动作建议变成程序调用
  6. 多步任务需要不同的控制策略
  7. 状态保存事实,上下文提供当前视图
  8. 框架把闭环沉淀为可维护的系统
  9. 生产系统必须约束失败方式
  10. 根据任务的不确定性选择最小架构
  11. 评价
    1. 写得好的地方
    2. 可以改进的地方

从会回答问题到能完成任务

聊天模型与 Agent 的区别,不在于回答听起来有多聪明,而在于系统能否读取外部事实、执行动作,并根据动作结果继续推进任务。

假设用户提出一个看似简单的请求:“根据今天的北京天气,推荐三个适合游览的景点。”单次模型调用只能依据训练时学到的常识作答,既不知道此刻是否下雨,也无法核实景点是否开放。一个能够真正完成任务的系统至少要经历三个阶段:

  1. 查询北京的实时天气;
  2. 根据天气决定应该寻找室内还是室外景点;
  3. 查询符合条件的景点,并核对开放信息后生成建议。

这三个阶段不能在调用模型之前全部写死。天气是执行时才获得的新事实,而新事实会改变下一步动作:晴天可以搜索公园和历史街区,暴雨则应转向博物馆并检查预约情况。当后一步依赖前一步的真实结果时,任务就从一次文本生成变成了连续决策。 Agent 正是承载这种连续决策的系统。

Agent 是一个受控的行动闭环

Agent 不是一种新的模型,而是一套围绕模型运行的控制结构:程序把目标和当前状态交给模型,模型提出下一步动作,程序校验并执行动作,再把结果带回下一轮。

flowchart TB
    G["目标:按实时天气推荐北京景点"] --> C["控制器:装配状态与可用工具"]
    C --> M["模型:提出下一步动作"]
    M --> P["协议:生成结构化调用"]
    P --> V{"参数、权限和预算通过?"}
    V -- "否" --> E["记录错误或请求人工处理"]
    V -- "是" --> T["工具:读取或改变外部环境"]
    T --> O["Observation:结构化执行结果"]
    O --> S["更新任务状态"]
    S --> D{"任务完成或必须停止?"}
    D -- "继续" --> C
    D -- "完成" --> R["交付结果"]
    D -- "超限或高风险" --> E

闭环里的组件各有清晰边界:

组件负责什么不应该负责什么
模型理解模糊目标,提出计划、工具和参数直接获得系统权限
协议把动作表达为程序可以解析的数据判断业务上是否允许执行
工具查询数据或产生真实副作用自行决定整个任务如何推进
状态保存目标、进度、观察和预算把全部历史无差别塞给模型
控制器装配上下文、校验动作、驱动循环把模型输出当成可信命令
验证与退出机制判断结果是否合格、何时停止只依赖模型的一句“已完成”

因此,“给聊天模型接上几个 API”只是提供了动作能力,还没有形成可靠的 Agent。缺少状态,系统会忘记已经查过什么;缺少验证,错误结果会进入下一轮;缺少停止条件,模型可能反复调用同一个工具。要理解这些边界为什么必要,首先要看清模型在闭环中到底做了什么。

大语言模型只负责提出下一步

大语言模型进入闭环后扮演的是概率决策器:它根据当前上下文(这一轮交给模型的全部信息)预测接下来的 token(模型处理文本时使用的基本片段),并让一串 token 表现为计划、工具名称、参数或最终答案。

对位置 $t$ 的 token,模型学习的仍是条件概率:

\[P(x_t \mid x_1, x_2, \ldots, x_{t-1})\]

当上下文同时包含用户目标、可用工具说明、当前状态和之前的执行结果时,“最合适的后续文本”就可能是“调用天气查询工具,并把城市设为北京”。模型不需要内置一个真正的天气模块,它只需要学会在合适的语境中生成符合工具约定的动作。

这种能力很适合处理难以提前写成规则的部分:理解自然语言中的隐含目标、在多个工具中选择一个、根据新证据调整计划。它的边界同样明确:能够生成一个动作,不代表动作存在、参数正确、调用获准或结果可信。 即使输出在语法上完全合法,也可能查错日期、选择越权工具,或者在没有充分证据时宣布任务已经完成。

现代 Agent 因而是概率模型与确定性程序的组合。模型处理开放式判断,程序负责类型、权限、预算、执行和验证。两者之间还缺少一个明确接口,Function Calling 就是为这个接口服务的。

Function Calling 把动作建议变成程序调用

Function Calling(函数调用)是一种结构化协议:程序先用 JSON Schema(描述字段类型和约束的机器可读规则)声明可用工具及其参数格式,模型再返回工具名称和参数,由控制器决定是否执行。

在旅行助手中,模型可能先返回:

1
2
3
4
5
6
7
{
  "name": "get_weather",
  "arguments": {
    "city": "北京",
    "date": "2026-08-04"
  }
}

这段 JSON 不是已经发生的动作,只是动作提议。控制器要确认 get_weather 已注册、城市和日期符合 Schema、调用没有超过预算,然后才执行工具。工具返回“中雨”后,控制器把必要结果作为 Observation(观察结果)写回状态,模型才有依据继续调用室内景点搜索工具。

sequenceDiagram
    participant C as 控制器
    participant M as 大语言模型
    participant V as 参数与权限校验
    participant T as 工具执行器
    participant E as 外部环境

    C->>M: 目标 + 当前状态 + 工具 Schema
    M-->>C: tool_call(name, arguments)
    C->>V: 校验工具、参数、权限和预算
    alt 校验通过
        V->>T: 执行已注册工具
        T->>E: 查询或修改环境
        E-->>T: 原始结果
        T-->>C: 结构化 Observation
        C->>M: 回注必要结果,进入下一轮
    else 校验失败
        V-->>C: 拒绝原因或可恢复错误
        C->>M: 要求修正动作或停止
    end

一个可用的工具定义至少包含五部分:稳定名称、使用场景说明、JSON Schema、本地实现,以及超时、权限、幂等(重复请求不会重复产生副作用)和输出上限等运行约束。工具描述既要告诉模型“何时使用”,也要说明“何时不要使用”;否则多个语义相近的工具会让选择变得含糊。

在 Java 等静态语言中,可以用显式注册表把工具名映射到实现和参数类型,也可以在启动阶段通过注解生成注册信息。无论采用反射、函数接口还是框架注解,运行时都要经过“反序列化—校验—寻址—执行—序列化”。类型系统能减少格式错误,但不能替代业务权限和副作用控制。

Function Calling 解决了单个动作怎样安全落地,却没有决定多个动作如何组织。天气、景点和开放时间需要连续查询,控制器还要知道先做什么、何时改计划,以及结果不合格时是否重试。

多步任务需要不同的控制策略

当任务不能由一次工具调用完成时,Agent 需要控制策略来决定闭环怎样前进;ReAct(推理与行动交替)、Plan-and-Solve(先规划再执行)与 Reflection(反思改进)分别处理探索、规划和改进三个问题。

策略怎样控制闭环适合什么任务主要风险
ReAct推理、行动、观察交替进行路径未知,需要根据新事实调整串行调用多,可能重复或陷入局部循环
Plan-and-Solve先拆解目标,再逐步执行子任务和依赖关系相对清楚初始计划可能被新证据推翻
Reflection根据评测结果生成反馈并重做有明确质量信号、允许增加延迟自我评价可能重复原有盲点

ReAct 让 reasoning(判断下一步)与 acting(执行工具)交替发生。在旅行场景中,模型先查天气,看到中雨后再决定搜索博物馆,而不是提前猜测搜索方向。它适合路径未知的任务,但必须设置 max_steps、无进展检测和费用上限,防止模型不断更换关键词却无法结束。

Plan-and-Solve 先把任务拆成“查天气—选择景点类型—搜索候选项—核对开放时间—生成建议”。计划提供全局方向,但不应成为不可修改的剧本;如果天气工具返回极端天气预警,后续计划就应该改成安全提示,而不是继续机械推荐景点。

Reflection 在执行结果之后增加“评审—改进”循环。旅行助手可以检查候选景点是否确实适合雨天、是否有三个、开放时间是否覆盖计划时段。只有当评审来自规则、测试、外部数据或人工反馈时,反思才有可靠抓手;让同一个模型无证据地反复自评,可能只是更自信地重复错误。

三种策略可以按任务组合,而不是互相替代:

flowchart TB
    G["目标"] --> P["Plan:拆解阶段与完成条件"]
    P --> X

    subgraph X["每个阶段内部"]
        R["Reason:判断下一步"] --> A["Act:调用工具"]
        A --> O["Observe:读取结果"]
        O --> D{"阶段完成?"}
        D -- "否" --> R
    end

    D -- "是" --> V["Validate:用外部证据验收"]
    V --> Q{"质量达标?"}
    Q -- "否" --> C["Reflect:形成可执行反馈"]
    C --> P
    Q -- "是" --> F["交付"]

循环越长,新的问题就越明显:模型每轮需要知道哪些事实已经获得、计划执行到哪里、哪些失败不该重试。把完整执行历史不断追加到提示词,既昂贵又容易让关键信息淹没在噪声中,因此多步控制必须进一步引入状态与上下文管理。

状态保存事实,上下文提供当前视图

多轮 Agent 必须区分两个容易混淆的概念:状态是系统持久保存的任务事实,上下文是某一轮从状态和外部资料中挑选出来、真正交给模型的信息。

旅行助手的状态可以包含用户偏好、已查询的天气、候选景点、当前计划步骤、剩余预算和失败记录。下一轮模型未必需要看到所有字段:搜索室内景点时,当前天气和用户偏好很重要,早先失败调用的完整原始响应则可能只需保留错误类型。控制器应该根据当前步骤选择信息,而不是把“记得更多”等同于“提示词更长”。

一次稳定的上下文装配通常经过四步:

1
2
3
4
Gather(收集候选信息)
→ Select(按相关性、时效和预算选择)
→ Structure(按固定语义分区组织)
→ Compress(压缩冗余,保留约束和证据)

装配后的内容可以分为 [Policies][Task][State][Evidence][Tools][Output]。固定分区并不是为了排版整齐,而是为了回答三个工程问题:某条信息为什么进入模型、它属于事实还是指令、出现错误时应该回溯哪一部分。

当任务跨越单个上下文窗口时,可以组合三种手段:

  • 压缩整合:把旧历史变成高保真摘要,保留目标、关键决策、未解决问题和验证证据;
  • 结构化笔记:把进度、阻塞项和下一步存到模型上下文之外,需要时再读取;
  • 即时检索:只在当前步骤需要时读取文件、代码、日志或知识库,而不是预先加载全部材料。

ContextBuilder 这样的组件负责装配当前视图,NoteTool 可以把阶段状态保存为 Markdown 或 YAML,TerminalTool 则提供即时读取能力。这些组件解决的是信息供给问题,不自动提供安全性:能读取终端或文件的工具仍然必须受到目录、命令、网络和输出大小限制。

框架把闭环沉淀为可维护的系统

当模型、工具、状态和控制策略逐渐增多,框架的价值就不再是少写几行 API 调用,而是把职责拆成可以替换、测试、恢复和观察的模块。

一个完整的 Agent 框架通常包含六层:

  1. 模型适配层:统一消息格式、流式输出、重试和不同模型接口;
  2. 工具层:管理 Schema、注册、校验、执行和错误封装;
  3. 状态与上下文层:保存任务事实,并为每一轮装配必要信息;
  4. 控制层:实现循环、规划、路由、重试和终止;
  5. 安全层:限制权限、目录、网络、费用和高风险动作;
  6. 可观测性层:记录模型请求、工具调用、状态变化和验证证据。

在 HelloAgents 这样的轻量实现中,FunctionCallAgent 展示了最小工具循环:根据工具描述构造 Schema,解析模型生成的参数,完成类型转换并调用已注册实现。它适合理解 Agent 的机械骨架,但如果要支撑跨轮次、可恢复的复杂任务,还需要持久化状态、并发控制、取消、重放、审批和评估。

另一类框架会把控制逻辑显式建模为“状态—节点—边”。例如 LangGraph 中,节点读取状态并返回更新,边决定下一个节点,条件边承载循环和分支。显式状态图更适合需要审计、恢复和人机协作的流程,代价是开发者必须提前定义更多结构;对于一次工具调用就能完成的任务,这种复杂度并不划算。

框架让组件可复用,也会把模型的动作能力连接到更多真实系统。系统一旦拥有文件写入、数据库更新、消息发送或命令执行能力,问题的重点就从“能否运行”转向“失败时是否可控”。

生产系统必须约束失败方式

最小 Demo 只需证明闭环能够跑通,生产系统则必须提前定义每一种失败怎样被发现、限制和恢复。

失败方式必要约束
模型反复尝试,无法结束max_steps、时间与费用预算、无进展检测、人工升级
调错工具或参数Schema 校验、业务校验、工具白名单、清晰错误分类
重试导致重复写入幂等键、事务、动作日志与可恢复状态
工具结果诱导模型越权把工具输出视为不可信数据,隔离指令与证据,限制下一步能力
命令或文件访问逃逸进程或容器沙箱、最小权限、参数化执行、网络与目录策略
模型误判任务成功测试、规则、外部数据或人工验收等独立证据
长任务遗忘目标或重复工作持久化状态、结构化笔记、压缩与按需检索
出错后无法复盘记录请求、动作、观察、状态变化、费用和退出原因

高风险动作还需要显式审批。例如查询天气可以自动执行,购买门票、发送消息或修改数据库则应先展示动作对象、参数和潜在副作用,再由用户或策略引擎批准。权限应该授予工具执行器,而不是授予模型;模型输出即使满足 JSON Schema,也仍然是不可信输入。

命令执行尤其不能把“限制工作目录”等同于沙箱。若底层使用 shell=True 执行模型生成的字符串,命令替换、符号链接、管道和解释器都可能绕过简单的路径判断。更可靠的方式是使用隔离进程或容器、参数化命令、最小文件权限和明确网络策略。

可观测性则负责把这些约束串成证据链。on_llm_starton_tool_endon_agent_finish 一类回调不应只打印日志,而应记录每一轮看到了什么、为何允许某个动作、工具产生了什么副作用、验证依据是什么。只有运行轨迹能够被关联和重放,Agent 的故障才可能定位和修复。

根据任务的不确定性选择最小架构

Agent 架构不是越完整越好;应先判断任务的哪一部分无法提前确定,再只引入解决这种不确定性所需的能力。

任务特征建议起点
一次生成即可完成,没有外部副作用单次 LLM 调用,不建立 Agent 循环
步骤固定、分支确定普通工作流,LLM 只处理非结构化节点
只需一次或少量工具调用Function Calling + 显式状态
路径未知,需要根据新证据调整有步数上限的 ReAct 式循环
任务可拆解且依赖关系清楚Plan-and-Solve,并允许重新规划
结果质量重要且有明确评判信号执行后增加 Reflection 或独立评估
跨小时、跨窗口或跨执行单元外部状态、结构化笔记、压缩与即时检索

复杂度不是 Agent 能力的徽章,而是为了处理具体不确定性付出的成本。判断一个设计是否合理,可以看模型是否只承担无法预先写死的决策,而权限、执行、验证、预算和停止条件是否仍牢牢掌握在确定性程序中。

评价

写得好的地方

这套知识体系最有价值的地方,是没有把 Agent 简化成“更会思考的模型”,而是把它还原为模型、协议、工具、状态和控制器共同组成的行动闭环。这个视角能够自然解释 Function Calling 为什么只是一层协议、ReAct 为什么需要停止条件,以及上下文窗口为什么不能代替持久化状态。

从单次生成一路推进到生产约束,也让概念之间形成了明确的因果关系:模型只能提出动作,所以需要结构化调用;单个动作无法完成复杂任务,所以需要多步控制;循环增长会带来遗忘和噪声,所以需要状态与上下文;系统获得真实权限后,又必须增加审批、幂等、沙箱和可观测性。这条路径既适合建立整体认识,也能直接转化为架构检查清单。

可以改进的地方

这套体系更偏向机制解释,还不足以替代完整的实现教程。FunctionCallAgentContextBuilderNoteToolTerminalTool 展示了关键抽象,但从最小示例走向真实项目时,仍需要补充持久化数据结构、并发状态更新、取消与恢复、人工审批界面以及端到端测试等具体实现。

生产建议目前也以定性原则为主,缺少能够比较不同方案的量化评测。例如上下文压缩应该衡量关键信息保留率,工具循环应该统计成功率、平均步数、重复调用率和费用,Reflection 则需要比较增加的调用成本是否真正换来质量提升。没有这些任务级指标,就很难判断新增一层 Agent 机制是在解决问题,还是只是在增加系统复杂度。

本文由作者按照 CC BY 4.0 进行授权