前面的文章已经把一次模型调用组织成了有边界的工作流。再往前一步,常会遇到一个词:Agent。它不是“更聪明的聊天机器人”,而是一种由模型根据当前状态选择下一步动作、由程序执行动作并继续循环的应用结构。本篇只聚焦 Agent 的基本组成:模型、工具、记忆与循环。示例不依赖在线服务,用一个可运行的规则模型模拟决策过程,先把程序骨架看清楚,再替换成真实模型。

Agent 和单次调用有什么不同

普通调用通常是“输入提示词,得到文本”:应用预先决定所有步骤,模型只负责生成结果。Agent 则把部分步骤选择权交给模型:模型读取目标和已有状态,决定是直接回答,还是请求某个工具;程序执行工具后,把观察结果放回状态,循环继续。

一个最小 Agent 可以拆成四部分:

  • 模型(Model):根据目标、历史和工具说明,提出回答或下一步动作。
  • 工具(Tools):模型之外的能力,例如查询数据、计算数值或读取文件。真正执行权在应用程序。
  • 记忆(Memory):保存任务目标、历史消息和工具结果,让下一轮决策有上下文。
  • 循环(Loop):反复执行“决策—行动—观察”,并用停止条件限制运行范围。

“记忆”不等于一定要接数据库。本地列表就是短期记忆;只有需要跨请求、跨进程保存时,才需要持久化存储。四部分的边界越清楚,测试和排错越容易。

先用 Python 模拟模型决策

为了让示例可以离线运行,下面的 decide 函数暂时扮演模型。它只识别一个简单目标:计算两个数字的和。真实项目中,这个位置可以换成 SDK 调用,但返回给循环的结果最好仍保持类似的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json


def decide(memory: list[dict]) -> dict:
"""模拟模型:需要计算时请求工具,否则结束。"""
user_text = memory[0]["content"]
if "计算" in user_text and "加" in user_text:
numbers = [int(part) for part in user_text.split() if part.isdigit()]
if len(numbers) >= 2:
return {
"type": "tool_call",
"name": "add",
"arguments": {"a": numbers[0], "b": numbers[1]},
}
return {"type": "final", "content": "我只能处理‘计算 A 加 B’这个示例。"}


def add(a: int, b: int) -> int:
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError("参数必须是整数")
return a + b


TOOLS = {"add": add}

这里的 TOOLS 是程序维护的白名单,而不是让模型自由调用任意 Python 函数。arguments 仍然是不可信输入,工具内部要做类型检查。真实模型通常会以 JSON 参数或 SDK 对象返回工具请求,应用应先解析和校验,再执行。

实现决策—行动—观察循环

下面的循环把四个组成部分串起来。memory 保存用户目标、模型动作和工具观察结果;decide 读取它并选择下一步;TOOLS 执行动作;得到结果后回到下一轮。max_steps 是硬停止条件,防止模型反复请求工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def run_agent(user_text: str, max_steps: int = 3) -> str:
memory = [{"role": "user", "content": user_text}]

for step in range(max_steps):
decision = decide(memory)
memory.append({"role": "assistant", "content": json.dumps(decision)})

if decision["type"] == "final":
return decision["content"]
if decision["type"] != "tool_call":
return "停止:无法识别模型动作。"

tool = TOOLS.get(decision["name"])
if tool is None:
return "停止:工具不在白名单中。"
try:
result = tool(**decision["arguments"])
except (TypeError, ValueError) as exc:
return f"停止:工具参数无效:{exc}"

memory.append({
"role": "tool",
"name": decision["name"],
"content": json.dumps({"ok": True, "result": result}),
})

# 模拟模型在看到工具结果后组织最终答案
return f"计算结果:{result}"

return "停止:超过最大步数。"


if __name__ == "__main__":
print(run_agent("请计算 8 加 13"))

运行命令是 python agent_basics.py,会输出 计算结果:21。这个结果来自实际的加法函数,不是模型编造的数字。示例为了突出结构,在工具返回后直接结束;接入真实模型时,应把工具结果追加到消息历史,再发起下一次模型请求,由模型生成最终文本。

四个组成部分的职责边界

模型只做决策,不直接执行。 它可以建议调用 add,但不能因为生成了某个函数名就获得文件系统、网络或数据库权限。应用必须检查工具名、参数、用户权限和副作用;涉及删除、付款等动作时,还要加入人工确认。

工具只做一件明确的事。 工具应有窄接口、有限参数和可测试的返回值。不要提供一个可以执行任意代码的“万能工具”,也不要用 eval 把模型文本当程序运行。

记忆要区分短期和长期。 当前任务的消息、动作和观察结果属于短期记忆;用户偏好或业务资料才可能进入长期记忆。保存前要考虑隐私、过期时间和容量,不能无限追加完整历史。

循环必须可停止、可观察。 除步数上限外,还可以设置总耗时、工具调用次数、单次输出大小和总成本上限。每轮记录任务标识、动作名称、耗时和结果状态,但日志中不要写入密钥或未经脱敏的敏感数据。

常见问题

有循环就一定是 Agent 吗? 不一定。固定步骤的流水线也是循环,但如果下一步完全由程序预先写死,就更接近工作流。Agent 的特征是模型参与了下一步动作选择,同时仍受程序边界约束。

为什么示例中的模型这么简单? 这是为了隔离 Agent 的控制结构。先验证状态、白名单和停止条件,再接入具体模型,出现问题时才能判断是模型决策错,还是应用执行错。

工具结果应该直接拼进提示词吗? 可以传递,但应使用明确的结构和角色标记,并限制长度。工具返回的外部文本不能自动成为系统指令;应用还要防范其中夹带的恶意指令。

Agent 越自主越好吗? 不是。自主性增加的同时,权限、成本和不可预测性也增加。只读、低风险、可回滚的任务适合先自动化;高风险动作应保留审批和撤销机制。

小结

Agent 的最小骨架不是一个神秘的新组件,而是四个清晰的部分:模型负责决定下一步,工具提供受控能力,记忆保存状态,循环把决策和行动连接起来。实际开发时,应先用离线假模型验证这条链路,再接入真实模型,并始终保留白名单、参数校验和硬停止条件。下一步可以在这个骨架上实现“单 Agent 最小实现与停止条件”,进一步处理模型连续调用工具时的状态迁移。