📦 本文配套完整源码 + 意图路由框架 + 多技能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),但如果频繁出现:

  1. 给每个选项加描述(最有效)
  2. 在提示词里把选项名称用引号括起来
  3. 降低 temperature 到 0.0

模型总是选第一个选项?

这是 position bias(位置偏差)——模型倾向于选列表里第一个。

解决办法: 每次调用时随机打乱选项顺序:

import random

shuffled = choices.copy()
random.shuffle(shuffled)   # 打乱顺序
decision = decide(user_input, shuffled)

练习任务

  1. ✅ 用上面的代码跑通客服分流的例子
  2. ✅ 写一个有5个选项的决策系统(比如开发助手)
  3. ✅ 测试故意输入模糊的话(比如"嗯"“好的”“666”),看 AI 怎么处理
  4. 🔥 挑战题: 实现一个两级决策系统——先判断大类(技术/生活/其他),再判断具体意图(答案在公众号资源包中有完整实现)

下期预告

第五课:给 AI 装上工具箱 — 工具调用

学完这课,你将能让 AI:

  • 🔨 调用搜索引擎帮你查资料
  • 📧 发邮件、读数据库,不只是说话
  • 🌤️ 查询实时天气,不再只靠训练数据
  • ⚡ 把 AI 从"只会说话的嘴"变成"能动手干活的手"

第四课学会了"选",第五课学会了"用"。选对了工具 + 会用工具 = 真正的 Agent。


📢 获取完整源码 + 全套资源包

参照 第一课 尾部

如果你觉得这篇教程有帮助,点个「在看」让更多人看到~

本文为《手搓 AI Agent 从 0 到 1》系列教程第 4 课,持续更新中。回复「Agent」不错过每一课

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐