AI Agent 入门:从工具调用到执行循环,读懂一个能跑的 Python 示例

假设你给一个 AI 助手发了这样一条消息:“帮我查一下订单,再看看能不能退款。”几秒钟后,它回复:“已经查询完毕,可以退款。”

听起来很能干。但作为开发者,我们得多问一句:它真的查了订单,还是只是生成了一句很像查询结果的话?如果整个程序只有一次模型 API 调用,没有订单查询接口,也没有把订单信息提供给模型,那么这句“已经查询完毕”就不能作为查询发生过的证据。模型的表达能力再好,也不能用一句话替数据库完成一次读取。

理解 AI Agent,可以从这个很朴素的问题开始:怎样让模型不只是给出回答,而是参与一个有真实执行、有结果反馈、也有明确边界的任务流程?

本文先拆开工具调用与执行循环,再运行 Tiny-Agent 中的一个小例子。最后,我们把这些机制放回一条完整的学习路线里。

一、先分清模型与 Agent:一个负责生成,一个系统负责把事情办下去

这里讨论的是大模型驱动的软件 Agent。一个实用的理解方式是:系统围绕用户目标,让模型根据当前信息提出下一步,通过工具与外部环境交互,再依据执行结果决定继续、调整还是结束。不同项目对 Agent 的定义并不完全一致,但“下一步是否由模型动态参与决定”是一个有用的观察角度。[1]

注意,主语是“系统”,不是“模型”。模型可以判断需要查订单,应用程序负责判断这个订单是否属于当前用户、查询参数是否合法、应该调用哪个接口。查询结果返回之后,模型才能基于这些新信息继续处理任务。[2]

可以把它想成一位能阅读材料、提出建议的助理。你会给他业务资料,也会让他申请使用一些工具,但不必因为他表达流畅,就顺便把公司的所有钥匙都交出去。越是可能产生实际影响的操作,越需要由程序明确控制执行条件。

这也说明,并不是所有用了模型的程序都需要采用自主循环。如果任务始终是“查订单 → 读取政策 → 生成说明”,固定工作流就可能足够。只有当下一步确实需要结合中间结果动态判断时,增加 Agent 式控制才有意义。多引入一次模型决策,就多了一次调用以及相应的延迟、成本和不确定性,不能只看架构图是不是更热闹。[1]

二、Tool Calling:模型拿到的是申请表,不是遥控器

先用一个比退款简单的任务理解工具调用:读取东京的教学天气记录,再把摄氏温度转换成华氏温度。这里使用固定数据,不是实时天气;我们要观察的是程序如何协作,而不是今天出门要不要带伞。

在 Tiny-Agent 的 Stage 01 中,模型侧提出的调用会被表示成一个 ToolCall 对象。一次请求可以长这样:[3]

ToolCall(
    call_id="call-weather",
    name="get_teaching_weather",
    arguments={"city": "Tokyo"},
)

name 表示想使用哪个工具,arguments 是传给工具的参数,call_id 用来把这次调用与随后返回的结果对应起来。它们描述的是一次执行请求,而不是“工具已经运行成功”的证明。自定义函数工具的常见调用流程,正是模型提出请求、应用执行函数,再把结果返回给模型。[2]

接下来才轮到应用程序处理。在这个例子里,工具注册表负责按名称查找已经登记的工具;不存在的名字会被拒绝,而不是拿去动态执行:[3]

def execute(self, call: ToolCall) -> Any:
    tool = self._tools.get(call.name)
    if tool is None:
        raise UnknownToolError(f"Unknown tool: {call.name}")
    return tool.invoke(call.arguments)

进入 invoke() 后,参数还会经过 Pydantic 数据模型校验,再交给真正的 Python 处理函数。比如天气工具只接受示例支持的城市,不能因为模型写了一个听起来很有地理气息的“Atlantis”,程序就假装自己接通了亚特兰蒂斯气象台。[3]

这里有一个容易混淆的地方:参数符合结构要求,不代表对应操作就一定应该执行。城市名称的格式校验,与真实业务里的用户权限检查,并不是同一回事。同样,call_id 用于关联调用和结果,也不等于为退款这类业务操作自动提供了幂等保证。工具调用解决的是“怎样表达请求”,业务规则还要由应用负责。[2][3]

三、Agent 的核心循环:执行完以后,还要把结果交回去

工具能被调用之后,问题还没有结束。

查到东京的温度是 18°C,只完成了用户任务的一半。系统还需要把这个结果提供给下一次决策,让它继续提出温度换算请求;换算完成,再根据结果生成最终回答。如果每次都由开发者提前写好“第一次模型调用”“第二次模型调用”“第三次模型调用”,任务一变,代码就得跟着重新数步子。

Stage 01 把这件事放进了一个有上限的执行循环。核心入口只有两行:[3]

for step in range(1, self.max_steps + 1):
    turn = self.model.generate(messages, self.registry.schemas())

messages 保存当前任务记录,schemas() 提供可用工具的描述。模型侧返回最终文字时,运行时结束;返回工具调用时,运行时记录调用、执行工具,再把结果追加回任务记录。下一轮决策读取的输入因此与上一轮不同。

工具结果的记录方式如下:[3]

messages.append(
    {
        "role": "tool",
        "tool_call_id": call.call_id,
        "name": call.name,
        "content": observation,
    }
)

这就是反馈为什么重要:执行函数本身不会让模型“自动知道”发生了什么,结果必须进入后续调用能看到的输入。只执行、不回传,就像让同事去查材料,却在他回来时把门锁了,然后问他为什么还不知道答案。[2][3]

把它连起来,流程其实并不神秘:

用户提出任务
    ↓
模型侧给出本轮决定 ←────────────┐
    ├─ 最终回答 → 结束           │
    └─ 工具调用 → 验证并执行     │
                      ↓        │
                 记录工具结果 ──┘

当然,不能让这个循环永远转下去。示例里的 max_steps 限制的是模型决策轮数:到达上限还没有结束,就抛出明确异常。它不是工具调用总数限制,也不是“所有工具必须在几秒内完成”的超时机制。理解一个保护措施具体限制了什么,比看见代码里有个 max 就放心更重要。[3]

四、把例子跑起来:先看清过程,再接真实模型

上面这些机制来自我整理并开源的 Tiny-Agent。它不是让你安装完就交付客户的业务平台,而是一门从基础机制逐步进入 Agent 工程问题的课程。[4]

项目地址:github.com/wds1996/Tiny-Agent

准备好 Python 3.10 或更新版本,建议在自己的虚拟环境中执行:

git clone https://github.com/wds1996/Tiny-Agent.git
cd Tiny-Agent

python -m pip install -r stages/01-react-runtime/code/requirements.txt
python stages/01-react-runtime/code/runtime.py

这个入口使用的是 ScriptedWeatherModel:一个按预设规则返回结果的模型替身,不会请求在线大模型,也不需要模型服务的 API Key。它用来检查运行时能否正确传递数据、执行工具和结束任务,不能用来证明真实模型已经具备任务理解能力。[3]

按示例的固定数据,关键运行轨迹如下:

[1] ACTION  get_teaching_weather({'city': 'Tokyo'})
[1] OBSERVE {"city": "Tokyo", "temperature_c": 18.0, "condition": "cloudy"}
[2] ACTION  celsius_to_fahrenheit({'temperature_c': 18.0})
[2] OBSERVE {"temperature_f": 64.4}
[3] FINAL   Tokyo's deterministic teaching record is 18.0°C (64.4°F), cloudy.

这里值得观察的不是最终那句英文,而是中间两次往返。先读取天气,得到 18.0;再把这个数交给换算工具,得到 64.4;最后才输出答案。你可以顺着日志追踪每个数值从哪里来,而不是只看见一个结论。[3]

接着运行检查:

python stages/01-react-runtime/code/runtime_checks.py

里面不仅检查正常结果,也检查未知工具、非法参数、重复调用编号、工具异常和无法结束的循环。读这些检查时可以反过来想:假如模型给错了东西,程序究竟在哪一步拦住它?这比只把正常示例跑通,多学到一层执行边界。[3]

仓库也提供真实模型适配示例,需要按照对应章节配置服务密钥和模型名称。离线替身用于验证控制逻辑,真实模型用于验证模型交互,两者回答的是不同问题,不要互相冒充。[3][4]

五、为什么一个小循环,最后会长成一门工程课程?

把工具循环跑通以后,很容易产生一种错觉:Agent 好像已经写完了。可只要把天气换成真实业务,新问题就会接着出现。

假设用户问的是公司的退款政策。模型训练时未必见过这份内部文档,系统就需要先检索相关资料。这是检索增强生成,也就是 RAG,要解决的一类问题。再往前走一步,即使找到了相关文档,也还要判断它是否足够回答当前问题,不能把“搜到了”直接翻译成“证实了”。Tiny-Agent 在 Stage 04 沿着这条路径展开。[4]

如果任务执行到一半需要人工审批,或者进程突然退出,之前存在内存里的变量又该怎么办?于是需要把执行状态保存下来,明确从哪里恢复、哪些操作已经完成。与此同时,用户的长期偏好和当前任务的运行状态也不能混在一起。这些问题进入了记忆、持久化和人工介入的章节。[4]

等系统能够保存越来越多的信息,又会遇到新的麻烦:保存过,不等于每一轮都应该发送给模型。上下文该选择什么,哪些资料按需加载,哪些内容不能当成指令,需要继续单独处理。再之后,工具失败怎么重试、效果怎么评估、长任务怎么换一个工作进程继续,就都不再是“把提示词写长一点”能解决的事情。[4]

Tiny-Agent 目前按 00–15 编排了 16 个阶段,学习路径大致可以分成四段:[4]

学习阶段主要内容要解决的问题
Stage 00–03模型调用、结构化输出、工具循环、工作流、状态编排程序怎样决定下一步,又怎样把决定执行出来?
Stage 04–08RAG、MCP、记忆、上下文工程、Agent Skills任务需要哪些信息和能力,怎样在合适的时候提供?
Stage 09–12可靠性与安全、评估与可观测性、多 Agent、工作区与沙箱行为更复杂以后,怎样验证效果并守住执行边界?
Stage 13–15服务化、长任务恢复、客服 Agent 综合示例怎样把这些机制放进一个能够持续推进的业务流程?

这个顺序不是为了凑齐技术名词。比如状态编排会先展示普通 Python 机制,再映射到 LangGraph;检索先解释数据如何被切分和比较,再进入向量后端。这样看到框架接口时,你能把它对应回已经理解的工作,而不是只能记住一串参数。[4]

六、最后做的不是“万能 Agent”,而是一个能说清边界的业务示例

课程的综合示例是一个客服 Agent:回答退款政策问题、读取当前用户的订单,以及在满足条件时提出退款申请。涉及退款动作时,需要人工审批,并检查相应的执行条件。

这比“一个 Agent 帮你完成一切”听起来朴素,但很适合检验前面学到的东西。它有没有读错人的订单?回答是否有政策依据?审批拒绝后有没有产生副作用?重复执行时会不会重复记录退款?这些都是可以落到程序行为上的问题。示例中的退款在本地业务存储中模拟,不是已经接通了真实支付渠道。

仓库提供中英文教程,完整示例与可执行检查放在对应章节。只要会基础 Python,就可以从 Stage 00 开始;读到工具循环时,再回来重新看一遍本文的运行轨迹,会更容易理解那些抽象为什么出现。

我希望 Tiny-Agent 帮助读者建立的,不是“我会调用某个 Agent 框架”的熟练度,而是面对一个任务时,能先解释清楚:模型负责判断什么,程序负责保证什么,以及这一次执行到底发生了什么。

项目地址:Tiny-Agent。觉得这条学习路线有帮助,欢迎 Star;运行时遇到问题,也欢迎带上复现步骤提交 Issue。让一个示例少一些“看起来能跑”,多一些“可以解释为什么这样跑”,就是值得做的改进。

参考资料

[1] Anthropic:Building Effective Agents

[2] OpenAI:Function calling

[3] Tiny-Agent Stage 01,重点参考 code/runtime.py、code/runtime_checks.py 与模型适配示例。

[4] Tiny-Agent 中文主页与课程导航

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐