
本文收录于专栏 《AI Agent原理与实战》2026版 ------ 专栏系统覆盖 AI Agent 的记忆、工具、插件与实战,点击订阅可跟踪后续更新。
阶段 1|概念与基础 | 来源:【补】(吸收微软课程微·04 Tool Use 设计模式六要素与 trustworthy 考量的思想) | 知识更新至 2026-10-01(写作当日检索+实测)
本课定位:五课概念备齐,第一次动手。本课不依赖任何框架,用约 100 行纯 Python + 国产模型 API 从零写出一个能查天气、能算数的 Agent------把第 5 课的四大件落成可运行的代码。这个最小循环是全课程的锚点:第 16 课用它对比框架、第 17 课用它迁移 MAF,后面所有框架课都回指它。
关键字: Agent开发入门、最小Agent Loop、纯Python实现、大模型API调用、工具调用实战、零框架开发、代码详解、动手实战
本节目录
- [6.1 Agent Loop 的最小充分集](#6.1 Agent Loop 的最小充分集)
- [6.2 设计决策:为什么"这样"写](#6.2 设计决策:为什么"这样"写)
- [6.3 读代码:逐段对应四大件](#6.3 读代码:逐段对应四大件)
概念讲解
6.1 Agent Loop 的最小充分集
把第 1 课的定义(LLM + 工具 + 循环 + 环境反馈)翻译成代码要件,一个最小 Agent 只需要四样:

四个要件分别对应前几课的知识点:工具注册表 = 第 2 课 function calling 的 schema 与执行分离;对话状态 = 第 2 课"messages 是一切";主循环 = 第 3 课 ReAct 的代码化(Thought 在模型内部、Action 是 tool_calls、Observation 是 tool 消息);护栏 = 第 4 课成本模型的最简体现。
终止条件只有两种 :模型不再发起工具调用(自然终止,tool_calls 为空),或达到最大迭代数(强制终止)。生产系统还要加第三种------预算耗尽/用户中断,这里从简。
6.2 设计决策:为什么"这样"写

为什么 schema 与实现分开注册? schema 是给模型看的说明书(进 prompt),实现是给运行时执行的(留在代码里)。两者用字典 TOOLS_IMPL[name] 关联。分离的意义:同一份实现可以配不同详细度的 schema(第 8 课 ACI 的伏笔------描述质量直接决定调用质量)。
为什么每个工具结果要截断打印? 环境返回可能巨大(一整个网页)。打印用 [:80] 只是控制台卫生;真正的截断/摘要策略属于上下文工程(第 13 课)。但回传给模型的内容本课不截断------最小实现优先正确性。
为什么系统提示词要写"必须调用工具,不要凭记忆作答"? 这是给模型的行为纪律(第 2 课 system 角色的用途):不写这句,模型可能直接用参数知识回答"8848 米",绕过工具------Agent 的可靠性一半靠这种显式约束。
为什么用 eval 还要做字符白名单? 教学最小实现。白名单把输入限制为数字和四则运算符,挡住 __import__ 之类注入。生产代码应换 ast.literal_eval 的安全算术解析或专用库------Agent 的工具是攻击面 (第 29 课)。这个设计与微软第 4 课 trustworthy 一节的原则同源:微·04 针对LLM 生成 SQL 的风险,给出的方案不是"禁止用 SQL"而是"数据库配只读(SELECT)角色"------不信任模型的输出,就收缩它能动用的权限,而非放弃工具能力。
6.3 读代码:逐段对应四大件

第 5 课的四大件在 mini_agent.py 中的落点:
| 四大件 | 代码位置 | 形态 |
|---|---|---|
| Planning | system 提示词 + 模型逐轮推理 | 隐式(无独立 planner) |
| Memory | messages 列表 |
窗口内短期记忆 |
| Tool Use | TOOLS_SCHEMA + TOOLS_IMPL + 执行分支 |
2 个工具,本地函数 |
| Reflection | 模型对观察的自评(决定"信息够了") | 隐式,靠模型能力 |
| 循环+护栏 | run_agent 的 for 循环 + MAX_ITERS |
显式 |
案例实战:跑通"搜索 + 计算器"双工具 Agent
目标:运行 mini_agent.py,观察完整的工具决策链与循环退出行为。
环境 :Python 3.8+(仅标准库 urllib/json/sys);模型端为任意 OpenAI 兼容 /v1/chat/completions 服务。实测配置:Ollama 0.34 + glm-4.7-flash(本机用的是其 64K 上下文自建变体 tag glm-4.7-flash-100k,官方 tag 为 glm-4.7-flash,权重相同);换成云端 API 只需改 API_URL/MODEL/API_KEY 三行。
完整代码 (stage1/code/mini_agent.py,92 行含注释):
python
# -*- coding: utf-8 -*-
"""最小 Agent Loop:纯 Python + OpenAI 兼容 API,无任何框架。
工具:calculator(真算术)+ search(本地迷你知识库,模拟检索)。"""
import json
import sys
import urllib.request
# ---------- 配置 ----------
API_URL = "http://localhost:11434/v1/chat/completions" # OpenAI 兼容端点
API_KEY = "YOUR_API_KEY" # 本地服务可不填真实值
MODEL = "glm-4.7-flash:latest" # 官方 tag(实测用同权重 64K 变体 -100k)
MAX_ITERS = 8 # 最大迭代保护
# ---------- 工具实现(Agent 的"手") ----------
KB = { # 迷你知识库:真实系统里这里是向量检索 / 网页搜索 API
"光速": "光在真空中的速度约为 299792458 米/秒,约 3×10^8 m/s。",
"长江": "长江全长约 6300 千米,是亚洲第一长河。",
"珠穆朗玛峰": "珠穆朗玛峰海拔约 8848.86 米(2020 年测定),是世界最高峰。",
}
def calculator(expr: str) -> str:
"""安全的四则运算:只允许数字和 + - * / ( ) . 空格"""
if not all(c.isdigit() or c in "+-*/(). " for c in expr):
return "错误:表达式含非法字符"
try:
return str(eval(expr, {"__builtins__": {}}, {})) # 受限 eval
except Exception as e:
return f"错误:{e}"
def search(query: str) -> str:
for key, val in KB.items():
if key in query:
return val
return "未检索到相关内容。"
TOOLS_IMPL = {"calculator": calculator, "search": search}
# ---------- 工具 schema(告诉模型有什么工具可用) ----------
TOOLS_SCHEMA = [
{"type": "function", "function": {
"name": "calculator",
"description": "计算四则运算表达式的精确值,输入如 '12*34+5'",
"parameters": {"type": "object",
"properties": {"expr": {"type": "string", "description": "算术表达式"}},
"required": ["expr"]}}},
{"type": "function", "function": {
"name": "search",
"description": "检索知识库,可查询光速、长江、珠穆朗玛峰等事实",
"parameters": {"type": "object",
"properties": {"query": {"type": "string", "description": "检索关键词"}},
"required": ["query"]}}},
]
# ---------- Agent Loop 本体 ----------
def call_llm(messages):
body = json.dumps({"model": MODEL, "messages": messages,
"tools": TOOLS_SCHEMA, "temperature": 0.2}).encode()
req = urllib.request.Request(API_URL, data=body,
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"})
with urllib.request.urlopen(req, timeout=120) as r:
return json.load(r)["choices"][0]["message"]
def run_agent(question: str):
messages = [
{"role": "system", "content": "你是一个会使用工具的助手。需要精确计算或事实检索时必须调用工具,不要凭记忆作答。"},
{"role": "user", "content": question},
]
for i in range(1, MAX_ITERS + 1):
msg = call_llm(messages)
tool_calls = msg.get("tool_calls") or []
if not tool_calls: # 模型不再调工具 → 最终答案
print(f"[迭代 {i}] 最终回答:\n{msg.get('content', '')}")
return msg.get("content", "")
messages.append(msg) # 记住模型的决策(含 tool_calls)
for tc in tool_calls: # 执行每个工具调用并回传结果
fn = tc["function"]["name"]
args = json.loads(tc["function"]["arguments"])
print(f"[迭代 {i}] 调用 {fn}({args})")
result = TOOLS_IMPL[fn](**args)
print(f"[迭代 {i}] 观察 ← {result[:80]}")
messages.append({"role": "tool", "tool_call_id": tc["id"],
"content": str(result)})
print("已达最大迭代次数,强制终止。") # 防失控保护
if __name__ == "__main__":
q = sys.argv[1] if len(sys.argv) > 1 else "珠穆朗玛峰的高度是多少米?换算成英尺呢(1米=3.28084英尺)?"
print(f"问题:{q}\n" + "-" * 50)
run_agent(q)

【已实测】(2026-10,Ollama 0.34 + glm-4.7-flash-100k,消费级 GPU 环境)运行:
bash
python mini_agent.py "珠穆朗玛峰的高度是多少米?换算成英尺呢(1米=3.28084英尺)?"
实际输出:
问题:珠穆朗玛峰的高度是多少米?换算成英尺呢(1米=3.28084英尺)?
--------------------------------------------------
[迭代 1] 调用 search({'query': '珠穆朗玛峰高度'})
[迭代 1] 观察 ← 珠穆朗玛峰海拔约 8848.86 米(2020 年测定),是世界最高峰。
[迭代 2] 调用 calculator({'expr': '8848.86*3.28084'})
[迭代 2] 观察 ← 29031.6938424
[迭代 3] 最终回答:
根据查询结果,珠穆朗玛峰的高度是**8848.86米**。
换算成英尺:
- 8848.86米 × 3.28084 = **29031.69英尺**(约29032英尺)
步骤一:解读轨迹 ------3 次迭代完成一次完整 ReAct 循环:迭代 1 模型判断缺事实(查库)、迭代 2 判断需要精确计算(调计算器)、迭代 3 判定信息充分,tool_calls 为空,循环自然退出。注意迭代 2 的智慧:模型没有 一步调用 calculator("8848.86*3.28084") 就完了,而是先取回真实高度再算------顺序决策由模型自主完成,这正是"结构交给模型"的含义(第 1 课)。
步骤二:做个破坏性实验(【已实测】同日)------把问题换成知识库没有的:
bash
python mini_agent.py "黄河的长度是多少千米?"
# [迭代 1] 调用 search({'query': '黄河长度'}) → 未检索到相关内容。
# [迭代 2] 调用 search({'query': '黄河'}) → 未检索到相关内容。
# [迭代 3] 调用 search({'query': 'Yellow River length'}) → 未检索到相关内容。
# [迭代 4] 最终回答:抱歉,我通过搜索工具没有找到黄河长度的相关信息......
这段失败轨迹比成功轨迹更有教学价值:模型连续三次改写检索词(精确词→短词→英文译词)仍无结果后,选择诚实承认"没查到"而不是编造 5464 千米------查询改写与放弃判断都是模型自主完成的,且环境反馈("未检索到")始终在驱动下一步决策。对比:若系统提示词去掉"必须调用工具"的纪律,模型第一轮就会用参数知识直接回答------哪种行为正确取决于产品定位(这也是 mini 实现与 Agentic RAG 系统的分界讨论,第 11 课)。
步骤三:验证护栏 ------把 MAX_ITERS 改为 1 再跑原问题,得到 [迭代 1] ... 已达最大迭代次数,强制终止。------护栏生效。(【已实测】2026-10-01)
步骤四:Windows 环境运行指引(代码本身跨平台,此处补 Windows 的完整上手路径):
-
安装 Python 3.10+:微软商店搜 "Python 3.12" 一键安装,或官网 python.org 下载安装包(勾选 "Add python.exe to PATH")。验证:
python --version。 -
安装 Ollama:官网 ollama.com 下载 Windows 版安装包(OllamaSetup.exe),一路下一步;完成后系统托盘出现 Ollama 图标即服务已启动(默认端口 11434)。
-
拉模型并运行:
powershellollama pull glm-4.7-flash # 官方 tag,约 19GB,视网速 10-40 分钟;磁盘空间需 25GB+ python mini_agent.py "珠穆朗玛峰的高度是多少米?换算成英尺呢(1米=3.28084英尺)?" -
无独立显卡也能跑:glm-4.7-flash 是 MoE(混合专家)架构,激活参数小,纯 CPU 亦可运行(每轮推理约十几秒到几十秒);模型文件约 19GB,建议物理内存 ≥32GB,磁盘预留 25GB+;有 ≥8GB 显存的独立显卡体验更佳。
-
走云端 API 替代本地模型:把代码顶部三行改为
API_URL = "https://api.deepseek.com/v1/chat/completions"(或智谱/SiliconFlow 等),MODEL与API_KEY填对应值------Windows/Linux 步骤完全一致。
Windows 路径的说明为环境指引,未在 Windows 机器上实际执行(本课实测在 Linux 上完成);代码零平台相关依赖,预期行为一致。
步骤五:换一家国产模型跑同一个 Agent(云端 API 路线,Windows/Linux 步骤完全一致)------把代码顶部三行换掉即可:
python
# 方案 1:DeepSeek(本课已实测,2026-10)
API_URL = "https://api.deepseek.com/v1/chat/completions"
MODEL = "deepseek-chat"
API_KEY = "YOUR_API_KEY" # platform.deepseek.com 创建
# 方案 2:智谱 GLM(工具调用已实测通过,2026-10)
API_URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
MODEL = "glm-4.7-flash"
API_KEY = "YOUR_API_KEY" # open.bigmodel.cn 创建
# 方案 3:MiniMax(工具调用已实测通过,2026-10)
API_URL = "https://api.minimaxi.com/v1/text/chatcompletion_v2" # 官方文档主推端点
MODEL = "MiniMax-M2.5"
API_KEY = "YOUR_API_KEY" # platform.minimaxi.com 创建
# 方案 4:小米 MiMo(按官方文档接入;api-key 头而非 Bearer,需微调 Authorization 那一行)
API_URL = "https://api.xiaomimimo.com/v1/chat/completions"
MODEL = "mimo-v2.6-pro"
API_KEY = "YOUR_API_KEY" # platform.xiaomimimo.com 创建;代码中 "Authorization": f"Bearer {API_KEY}" 改为 "api-key": API_KEY
【已实测】(DeepSeek 官方 API,Linux 发起)同一珠峰问题、同一份代码(只换三行配置)的实际输出:
问题:珠穆朗玛峰的高度是多少米?换算成英尺呢(1米=3.28084英尺)?
--------------------------------------------------
[迭代 1] 调用 search({'query': '珠穆朗玛峰 高度 米'})
[迭代 1] 观察 ← 珠穆朗玛峰海拔约 8848.86 米(2020 年测定),是世界最高峰。
[迭代 2] 调用 calculator({'expr': '8848.86*3.28084'})
[迭代 2] 观察 ← 29031.6938424
[迭代 3] 最终回答:
珠穆朗玛峰的高度为 **8848.86 米**(2020 年中尼联合测定,含雪面高度)。
换算成英尺:8848.86 × 3.28084 ≈ **29031.69 英尺**(约合 29032 英尺。)
对比本地 glm-4.7-flash 的轨迹:问题相同、工具相同、最终答案一致,但 DeepSeek 的检索词是"珠穆朗玛峰 高度 米"(本地模型是"珠穆朗玛峰高度")------模型换了,路径细节就换了。这正是第 1 课"Agent 同输入可能不同路径"的直接证据,也再次印证第 2 课的结论:tool_calls 协议是统一的,换模型只改三行配置。
预期输出与检查点 :跑通本课代码后,你应能回答四个问题------① tool_calls 为空意味着什么(自然终止)?② 工具结果靠什么字段配对(tool_call_id)?③ 若把 messages.append(msg) 删掉会怎样(模型看不到自己上轮决策,可能死循环重复调用)?④ 护栏为什么是必须品而不是可选项(第 4 课错误恢复账)。答得上来,说明前 5 课的概念真正落地了。
小结
- 最小 Agent 四要件:工具注册表、messages 状态、while 主循环、最大迭代护栏------100 行内全部实现。
- 终止条件两种:模型自然停止(tool_calls 空)+ 强制停止(MAX_ITERS);生产还需预算/中断两类。
- schema 给模型、实现给代码,用名字关联;描述质量就是调用质量(ACI 伏笔)。
- 系统提示词的行为纪律与受限执行环境都是安全的一部分(第 25/29 课展开)。
- 本课是全课程回指锚点:第 7 课加复杂工具、第 9 课加模式、第 16 课换框架、第 17 课迁 MAF------每次都回来对照"多了什么"。
扩展阅读
🌐 网络提示:anthropic.com 需代理(核心结论已摘进本课正文);openai.com 系需代理(协议层知识已进正文,实测均可用国产端点替代);huggingface.co 走镜像:
export HF_ENDPOINT=https://hf-mirror.com;ollama.com 索引页需代理,但ollama pull命令行拉模型多数网络可直连。详见总纲附录《国内网络访问与国产适配指南》。
- 本课代码文件:
stage1/code/mini_agent.py(可直接运行) - OpenAI Function Calling 指南(工具协议细节):https://developers.openai.com/api/docs/guides/function-calling (部分网络环境可能 403,可检索"OpenAI function calling guide"替代入口)
- HuggingFace Agents Course Unit 1(smolagents 的最小 CodeAgent 对照):https://huggingface.co/learn/agents-course/en/unit1/introduction (中国大陆网络建议镜像入口 hf-mirror.com 同路径访问)
- Anthropic《Building Effective Agents》"Agents"一节(loop + 环境反馈 + 停止条件的原始表述):https://www.anthropic.com/engineering/building-effective-agents
我的专栏
| 蛋白 / 抗体 / 多肽 / 核酸 | 分子模拟 / 动力学 / 对接 | 药物 / 设计 / 案例 | AI / Agent / 大模型 |
|---|---|---|---|
| 开源蛋白结构预测 | 分子模拟基础 | 小分子药物设计案例 | AI Agent系列 |
| 开源蛋白生成方法实践 | 分子动力学模拟-Amber | 蛋白药物设计案例 | 化学大模型 |
| 开源多肽设计模型 | 分子动力学模拟-Gromacs | 多肽药物设计案例 | 《AI Agent原理与实战》 |
| 开源多肽性质预测 | 分子动力学模拟-OpenMM | 开源小分子生成设计 | CADD中的机器学习模型 |
| DNA/RNA药物设计 | 結合自由能 | 高效计算配置 | 《MCP 入门实战》 |
| siRNA药物设计模型 | UCSF DOCK系列 | 我胡师兄说药 | 《AI入门实战2026》 |
| ASO药物设计模型 | rDock系列 LeDock系列 gnina系列 | 《经典机器学习实战》 |