【手搓 AI Agent 从 0 到 1】第四课:让 AI 学会自己做决定 — 决策与路由
📦 本文配套完整源码 + 意图路由框架 + 多技能Agent实战项目,已打包整理
前言:AI 不再只会"回答问题"了
前三课,你学会了两件事:
| 课 | 能力 | AI 的角色 |
|---|---|---|
| 第一课 | 跑通对话 | 一个会说话的模型 |
| 第二课 | System Prompt | 一个会换角色的模型 |
| 第三课 | 结构化输出 | 一个能输出稳定格式数据的模型 |
但不管怎样,AI 都只是在被动回答你的问题。你问什么,它答什么。
这节课开始不一样了:AI 要学会自己做选择。
举个例子:
用户说:"帮我总结一下这篇文章"
普通AI:直接开始总结(不管你说什么,它都回答)
带决策的AI:
1. 分析用户的意图 → 识别出"总结"
2. 从可选动作中选一个 → 选择了"summarize_text"
3. 路由到对应功能 → 调用摘要模块去执行
这就是"能动性"(Agency)的起点。 智能体不再只是回复,而是选择做什么。
💡 决策 = 能动性。选择,比回答更重要。
ChatGPT 和真正 Agent 的区别,就从这一步开始。
今天这课学什么?
| 目标 | 说明 |
|---|---|
| 🎯 本课产出 | 一个能自动判断用户意图并路由执行的决策系统 |
| 🔧 核心技能 | 决策原理、意图检测、条件路由、决策验证 |
| ⏱ 预计耗时 | 20 分钟跟着操作 |
前置要求: 已完成 第一课(环境搭建)+ [第二课](System Prompt)+ [第三课](结构化输出)
🔧 还没跟上?关注公众号「开源情报局」回复「Agent」,领取前三课完整源码包。
第一步:核心问题 — 怎么让 LLM 做"选择题"?
LLM 默认只会"作文题",不会"选择题"
前三课我们一直在让 LLM 做同一件事:给你一个问题,自由生成回答。这相当于做"作文题"——题目给定了,答案由你自己发挥。
但决策需要的是做选择题——给你 A、B、C 三个选项,你只能选其中一个,不能自己编 D。
作文题(前三课):
输入:"什么是 AI Agent?"
输出:(自由发挥,写300字)
难点:控制格式和内容质量
选择题(本课):
输入:"帮我翻译这段话"
选项:A. answer_question B. summarize_text C. translate
输出:C
难点:让 LLM 老老实实只选不编
怎么把"作文题"变成"选择题"?
核心思路:用第三课的结构化输出技术,把选项列表塞进 prompt 里。
具体来说,你需要在发给 LLM 的 prompt 中包含三样东西:
| 组件 | 作用 | 例子 |
|---|---|---|
| 选项列表 | 告诉 LLM 有哪些可选动作 | answer_question / summarize_text / translate |
| 每个选项的说明 | 让 LLM 理解每个动作是什么意思 | translate:翻译文本 |
| 输出格式约束 | 强制 LLM 只返回一个选项名 | {"decision": "选项名"} |
LLM 看到的完整 prompt 长这样:
你是一个意图识别助手。根据用户输入,从下面的可选动作中选择最合适的一个。
可选动作:
- answer_question:回答用户的技术问题或知识查询
- summarize_text:总结或浓缩用户提供的文本内容
- translate:翻译文本(默认中翻英)
规则:
1. 只能从上面的可选动作名称中选择一个,不能自己创造新名称
2. 只返回 JSON,不要有任何解释
3. JSON 格式:{"decision": "动作名称"}
用户输入:能不能帮我把这段英文翻译一下?
请返回 JSON:
LLM 的推理过程(简化版):
1. 用户说"翻译一下" → 和"translate:翻译文本"最匹配
2. 和其他两个选项不匹配
3. 按规则返回 JSON:{"decision": "translate"}
为什么要把"选项说明"也发给 LLM?
你可能想:只发 ["answer_question", "summarize_text", "translate"] 行不行?
行,但不稳定。 看这个对比:
| 发给 LLM 的内容 | LLM 的理解 | 准确率 |
|---|---|---|
translate |
“翻译?可能是 translate 吧…” | ⚠️ 靠猜 |
translate:翻译文本 |
“哦,translate = 翻译,确定了” | ✅ 确定 |
handle_faq:处理退款申请 |
“handle_faq 就是处理退款” | ✅ 即使名字不直观也能理解 |
⚠️ 动作名称是你的代码变量名,描述才是 LLM 的判断依据。 两者各管各的。
决策 Schema 长什么样?
结合第三课的知识,输出格式就是一个最简单的 JSON:
{"decision": "summarize_text"}
一个字段,一个值,值必须是选项列表里的某一个。
整个决策流程
用户输入:"能不能帮我把这段英文翻译一下?"
│
▼
┌─────────────────────────────────────────────┐
│ 第①步:构建 prompt │
│ │
│ 你把「选项列表 + 选项说明 + 输出格式约束」 │
│ +「用户输入」拼成一个完整的 prompt │
└──────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 第②步:发给 LLM(第三课的结构化输出技术) │
│ │
│ LLM 读取 prompt → 理解选项 → 对比用户输入 │
│ → 选出最匹配的选项 → 返回 JSON │
└──────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 第③步:验证结果 │
│ │
│ 检查返回值是否在选项列表中 │
│ 是 → 继续第④步 │
│ 否 → 重试(最多3次) │
└──────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 第④步:路由执行 │
│ │
│ 根据决策结果调用对应的处理函数 │
│ "translate" → 调用翻译模块 │
└─────────────────────────────────────────────┘
就这四步:构建 prompt → 发给 LLM → 验证结果 → 路由执行。 决策的核心原理就是这些。
💡 本质上,决策 = 第三课的结构化输出 + 选项列表约束。
你已经学会了结构化输出(第三课),这节课只是把它应用到一个特定场景:从有限选项中选一个。
第二步:基础实现 — 让 AI 做选择
代码思路
把上面讲的四步流程翻译成代码:
# 第①步:准备选项(名称 + 描述)
skills = {
"answer_question": "回答用户的技术问题或知识查询",
"summarize_text": "总结或浓缩用户提供的文本内容",
"translate": "翻译文本(默认中翻英)",
}
# 第②步:构建 prompt,发给 LLM
# 第③步:解析 JSON,验证结果
# 第④步:根据结果路由执行(第三步讲)
完整代码
import json
import re
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama"
)
def extract_json_from_text(text):
"""从文本中提取 JSON(第三课的复用工具)"""
try:
return json.loads(text)
except:
pass
match = re.search(r'\{.*\}', text, re.DOTALL)
if match:
try:
return json.loads(match.group())
except:
pass
return None
def decide(user_input, choices, model="qwen2.5:7b"):
"""
让 AI 从选项列表中选择一个动作。
参数:
user_input: 用户说的话
choices: AI 可以选择的所有动作列表
model: 使用的模型
返回:
选中的动作名称,如果全部失败则返回 None
"""
# 把选项列表拼成带描述的文本
options = "\n".join(f"- {choice}" for choice in choices)
# 技能列表放在 User Prompt 里(不是 System Prompt)
# System Prompt 是角色设定,应该保持稳定
# User Prompt 是每次请求的上下文,技能列表是动态的,放这里更合理
user_prompt = f"""你是一个意图识别助手。根据用户输入,从下面的可选动作中选择最合适的一个。
可选动作:
{options}
规则:
1. 只能从上面的可选动作名称中选择一个,不能自己创造新名称
2. 只返回 JSON,不要有任何解释
3. JSON 格式:{{"decision": "动作名称"}}
用户输入:{user_input}
请返回 JSON:"""
# 最多尝试 3 次
for attempt in range(3):
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一个有帮助的AI助手。"},
{"role": "user", "content": user_prompt}, # 技能列表在 user 消息里
],
temperature=0.0 # 温度0,决策必须稳定
)
text = response.choices[0].message.content
parsed = extract_json_from_text(text)
if parsed and "decision" in parsed:
decision = parsed["decision"]
# 关键:验证决策是否真的在选项列表中
if decision in choices:
return decision
print(f"❌ 第 {attempt + 1} 次尝试失败,重试中...")
print("❌ 3次尝试均失败")
return None
代码和原理的对应关系
把我们之前讲的决策流程和代码里的每一行对应起来:
| 决策流程 | 代码位置 | 对应的技术 |
|---|---|---|
| ① 构建选项+描述 | options = "\n".join(...) |
拼字符串 |
| ② 发给 LLM + 约束输出格式 | client.chat.completions.create(...) |
第三课的结构化输出 |
| ③ 解析 JSON | extract_json_from_text(text) |
第三课的 JSON 提取 |
| ③ 验证结果 | if decision in choices |
本课新增 |
| ③ 失败重试 | for attempt in range(3) |
第三课的重试模式 |
看出来了吗?第②步和第③步完全就是第三课的技术复用。 这节课真正新加的东西只有两样:
- 把选项列表塞进 prompt
- 验证返回值是否在选项列表中
来跑几个实际例子
# 基础版:只传动作名
my_skills = ["answer_question", "summarize_text", "translate"]
print(decide("量子计算是什么?", my_skills)) # → "answer_question"
print(decide("帮我总结一下这篇文章", my_skills)) # → "summarize_text"
print(decide("把这段话翻译成英文", my_skills)) # → "translate"
同样的代码,不同的输入,AI 自动选了不同的动作。 这就是决策。
第三步:加上路由 — 让决策变成行动
决策本身没意义,决策之后做什么才有意义。
路由器代码
def route(user_input):
"""
完整的意图识别 + 路由执行流程
"""
skills = ["answer_question", "summarize_text", "translate"]
# 第一步:让 AI 做决策
decision = decide(user_input, skills)
if decision is None:
return "❌ 无法理解你的意图,请换个说法试试"
# 第二步:根据决策路由到对应功能
print(f"🎯 识别意图:{decision}")
if decision == "answer_question":
return f"📝 [回答问题] 正在为你解答..."
# 这里调用你的问答模块
elif decision == "summarize_text":
return f"📋 [文本摘要] 正在生成摘要..."
# 这里调用你的摘要模块
elif decision == "translate":
return f"🌐 [翻译] 正在翻译中..."
# 这里调用你的翻译模块
return f"⚠️ 未知动作:{decision}"
# 完整流程测试
print(route("帮我翻译一下 Hello World"))
# 🎯 识别意图:translate
# 🌐 [翻译] 正在翻译中...
print(route("Python 的装饰器是什么?"))
# 🎯 识别意图:answer_question
# 📝 [回答问题] 正在为你解答...
路由的本质是什么?
就是 Python 的 if-else。 没有任何黑魔法:
# AI 选中了什么 → 调用对应的函数
if decision == "translate":
translate_module.run()
elif decision == "summarize_text":
summarize_module.run()
但这个 if-else 不是你写死的逻辑,而是 AI 根据用户输入动态选择的。用户说"翻译",AI 选 translate,代码走到翻译分支。用户说"总结",AI 选 summarize_text,代码走到摘要分支。
这就是 Agent 和普通程序的区别:普通程序的 if-else 是你写的,Agent 的 if-else 是 AI 选的。
第四步:对比一下 — 第三课 vs 第四课
同一个输入,两种模式
| 对比项 | 第三课(结构化输出) | 第四课(决策) |
|---|---|---|
| 你问:“帮我翻译这段话” | 返回 {"topic": "翻译", "confidence": "high"} |
返回 "translate" |
| AI 的自由度 | 高(自己填字段值) | 低(只能从列表选) |
| 输出可靠性 | 中(需要验证字段值) | 高(值必须是列表里的) |
| 后续操作 | 你自己根据输出写 if-else | 直接路由到对应功能 |
一句话总结
第三课让 AI “说话有格式”,第四课让 AI “知道该干嘛”。
格式化输出是基础,决策路由才是 Agent 的核心能力。
关键洞察
约束 = 可靠
这不是限制 AI 的能力,恰恰相反——选项越少,决策越准确。
| 选项数量 | 准确率 | 适用场景 |
|---|---|---|
| 2~3 个 | ⭐⭐⭐⭐⭐ 非常高 | 简单分流(是/否、A/B) |
| 4~6 个 | ⭐⭐⭐⭐ 高 | 常见意图分类 |
| 7~10 个 | ⭐⭐⭐ 一般 | 功能较多的助手 |
| > 10 个 | ⭐⭐ 容易混乱 | 建议分层决策(先粗分再细分) |
验证是安全网
永远不要直接用 AI 返回的决策值。 模型可能返回 "summarize" 而你的选项是 "summarize_text"。代码里必须检查 decision in choices,不在就重试。
选择 vs 生成 — 本质区别
| 生成(前三课) | 选择(本课) | |
|---|---|---|
| 输出空间 | 无限 | 有限且已知 |
| 可预测性 | 低 | 高 |
| 适用场景 | 聊天、写作、问答 | 任务调度、意图路由、工具选择 |
🎁 进阶内容:完整项目 + 更多玩法
上面是基础版原理展示。要做出真正能用的多技能 Agent,你需要:
公众号源码包中包含的内容:
| 内容 | 说明 | 文件 |
|---|---|---|
| 📦 完整 Agent 类(第四版升级) | decide() 方法集成到 Agent 类,支持带描述的技能注册 |
agent.py |
| 🔀 多技能路由框架 | 内置5个示例技能 + 可扩展的技能注册机制,加新技能只需写一个函数 | router.py |
| 🤖 完整 CLI 交互程序 | 支持连续对话 + 自动意图识别 + 技能切换的交互体验 | complete_example.py |
| 🧪 决策系统测试集 | 50+ 测试用例覆盖边界情况:模糊意图、中英混杂、恶意输入等 | 测试文档 |
| 📈 分层决策设计模式 | 10个选项以上的场景怎么处理?二级决策树方案 | 设计文档 |
| 🔧 生产级 Debug 手册 | “AI 总是选错怎么办?”“添加新技能的完整流程” | FAQ文档 |
👇 关注公众号「开源情报局」,回复「Agent」一键获取全部资源包:
实用决策模板速查
模板1:客服分流(2选1)
choices = ["human_agent", "self_service"]
# "我要投诉" → "human_agent"(转人工)
# "怎么重置密码" → "self_service"(自助服务)
模板2:内容处理(4选1)
choices = ["summarize", "translate", "classify", "extract_info"]
# "帮我把这段话缩写到100字" → "summarize"
# "这篇文属于什么类别" → "classify"
模板3:开发助手(5选1)
choices = ["write_code", "review_code", "fix_bug", "write_test", "explain_code"]
# "这个函数为什么报错" → "fix_bug"
# "帮我写单元测试" → "write_test"
模板4:智能路由(带兜底选项)
choices = ["search_docs", "query_database", "send_email", "unknown"]
# 兜底选项 "unknown" 很重要:
# 当 AI 确实不知道怎么选时,返回 "unknown"
# 你的代码可以回复 "我暂时不支持这个操作"
💡 完整模板 + 每个模板的扩展思路 → 公众号资源包获取
常见问题排查
AI 总是选错怎么办?
| 问题 | 解决方案 |
|---|---|
选项名称太相似(search vs search_web) |
改成差异更大的名字,或者在提示词里加说明 |
| 没给选项加描述 | 加上! LLM 靠描述做判断,不是靠猜名字 |
| 用户输入太模糊 | 加一个 unknown(兜底)选项 |
| 选项太多 | 拆成两级决策:先大类再小类 |
模型返回不在列表里的值?
代码层面已经做了保护(if decision in choices),但如果频繁出现:
- 给每个选项加描述(最有效)
- 在提示词里把选项名称用引号括起来
- 降低 temperature 到 0.0
模型总是选第一个选项?
这是 position bias(位置偏差)——模型倾向于选列表里第一个。
解决办法: 每次调用时随机打乱选项顺序:
import random
shuffled = choices.copy()
random.shuffle(shuffled) # 打乱顺序
decision = decide(user_input, shuffled)
练习任务
- ✅ 用上面的代码跑通客服分流的例子
- ✅ 写一个有5个选项的决策系统(比如开发助手)
- ✅ 测试故意输入模糊的话(比如"嗯"“好的”“666”),看 AI 怎么处理
- 🔥 挑战题: 实现一个两级决策系统——先判断大类(技术/生活/其他),再判断具体意图(答案在公众号资源包中有完整实现)
下期预告
第五课:给 AI 装上工具箱 — 工具调用
学完这课,你将能让 AI:
- 🔨 调用搜索引擎帮你查资料
- 📧 发邮件、读数据库,不只是说话
- 🌤️ 查询实时天气,不再只靠训练数据
- ⚡ 把 AI 从"只会说话的嘴"变成"能动手干活的手"
第四课学会了"选",第五课学会了"用"。选对了工具 + 会用工具 = 真正的 Agent。
📢 获取完整源码 + 全套资源包
参照 第一课 尾部
如果你觉得这篇教程有帮助,点个「在看」让更多人看到~
本文为《手搓 AI Agent 从 0 到 1》系列教程第 4 课,持续更新中。回复「Agent」不错过每一课
更多推荐


所有评论(0)