将 LLM 当作函数后, 我终于认识了 Agent
从毕业设计实践出发,拆解 AI Agent 背后的工程本质。揭示 LLM 只是"概率预测函数"的真相,探讨 RAG、Prompt、MCP、FSM 等技术如何弥补其天然缺陷,让"文本生成器"真正具备解决问题的能力。
我在学校做的毕业设计题目为 "基于 MCP 协议的多专家协作智能体系统设计与实现"
Github 地址为 Lian-MCP-LLM-Agent
其实算是我写的一个玩具项目, 目前因为生活繁忙暂停开发
但开发过程中确确实实学到了很多有用的东西, 也出现了很多有意思的想法
所以打算写一篇博客分享出来
📖 引言
本文将通过六个核心章节,还原我在构建 Lian-MCP-LLM-Agent 过程中的心路历程:LLM -> RAG -> Prompt -> MCP -> FSM -> CoT
这也正是我从零开始认识 Agent 的学习路径。建议按照编排顺序阅读,因为每一个后置技术的引入,往往都是为了解决前一个技术暴露出的缺陷。
第一章 🤖 LLM 的本质
大语言模型 (Large Language Model) 是目前生成式 AI 的核心技术。
简单来说,它是一种基于 Transformer 架构的深度学习模型,使用海量文本数据进行训练,拥有数十亿至数千亿的参数。
它利用自注意力机制(Self-Attention)分析上下文,擅长理解、总结、翻译以及生成文本与代码。
抛开 RAG、MCP、CoT 这些花哨的外部封装,回归最原始的视角
用程序员最好理解的方式来说,LLM 的本质其实就是一个函数:
fn llm(input: Text, context: Text) -> Token即:根据用户输入与上下文信息,预测并生成统计学上最可能的下一个词(Token)。
🌰 打个比方
LLM 生成的每一个字,都是从无数个岔路口中选择其中一条。
连续做 10 次选择,它就给你生成了一句 10 个字的话。
而 模型结构 (Transformer)、参数 (权重)、训练数据分布 共同决定了:
- 这些分岔路有多复杂?
- 路在何方?
- 哪些路比较宽(概率更高)?
所以,请记住这个残酷的事实:
- LLM 不是一个会 "主动思考" 的实体
- 它不理解你的目标,也不会为结果负责
- 它只是在给定
input和context的前提下,计算并返回一个概率最高的输出
🌲 LLM 的“短视”与局部最优
沿用上述的分岔路模型,想象生成过程就像是在走这样一棵树:
起点
├── A(概率高,现在看起来很好)
│ ├── A1(平平无奇)
│ └── A2(死胡同,逻辑崩坏)
│
└── B(概率低,现在看起来一般)
└── B1(逻辑严密,通向真理)当 LLM 站在起点时,它看不到终点 B1,它只能根据当前上下文的概率分布做单步选择。
于是,因为 A 的概率高,B 的概率低,它往往会毫不犹豫地选择 A。
最终它可能走到了 A1 甚至 A2,而错过了真正的全局最优解 B -> B1。
🤔 为什么平时用起来感觉它很聪明?
因为在海量的训练数据里,存在着无数的 统计巧合 和 人类经验的复用。
对于常见问题,由于人类语料库中本身就包含了正确的推导路径,LLM 只需要“背诵”或“模仿”这条路即可。
但一旦问题 太新、太长、逻辑链条太深,LLM 这种“贪婪”的本性就会暴露无遗。
这也是为什么 LLM 会有以下原生缺陷:
- 幻觉:一本正经地胡说八道(选择了概率高但错误的路径)。
- 短视:无法进行长远的规划和推理(看不到树的末端)。
- 无感:无法通过感知修正自己的错误(没有真实的感官输入)。
🧱 真正让 LLM 「能工作」 的,从来不是它自己
在真正开始写 Agent 之前,我也曾抱着侥幸心理迷信模型能力。
但当系统复杂度上来后,这种幻想很快就破灭了。
在这个章节,我的核心观点是:正因为 LLM 的本质如此单纯(甚至简陋),所以我们才需要外部系统。
在我的项目里,LLM 只占整个系统中非常小的一部分。占据 90% 甚至 99% 开发时间的,是这些东西:
- 状态机 / 自动机:强行矫正 LLM 的路径。
- 任务流编排:把大任务拆成 LLM 能处理的小任务。
- Prompt 工程:用精炼的语言给 LLM 指路。
- JSON 协议与校验:防止 LLM 输出不可以解析的垃圾。
- RAG / MCP:给“失忆”的 LLM 外挂硬盘和手脚。
LLM 在其中扮演的角色,其实非常单一:
在被精心准备好的
context中,理解“我现在在任务流里的位置”,然后生成下一步的建议。
换句话说:如果没有大量“固定架构”包住它,LLM 根本无法稳定工作。
🧠 去魅:那些听起来很厉害的名词
MCP、RAG、CoT、Agent、Skills……
这些词在最近被反复提起,甚至被过度神化。
但如果你真的理解了上文提到的 LLM 本质,你会发现它们几乎都在做同一件事:
弥补 fn llm(input, context) 这个函数的原生缺陷。
- RAG (检索增强):因为 context 长度有限且无法记住历史,所以我们需要“外挂硬盘”进行搜索。
- CoT (思维链):因为 LLM 本质是短视的概率预测,容易陷入局部最优,所以我们需要强制它“把思考过程写出来”,用显式的逻辑步骤对抗直觉。
- Agent (智能体):因为 LLM 无法通过单一路径解决复杂问题,所以我们需要循环、决策和工具调用。
- MCP (模型上下文协议):因为 LLM 无法感知真实世界,所以我们需要标准化的接口让它能“联网”和“操作数据库”。
不要惧怕这些词。
所谓“AI 时代的创新”,很多时候可能只是某个程序员为了让这个“概率预测机”更好地工作,而写的一段工程胶水代码。
名字不重要,解决 LLM 的本质缺陷且把事情做成,才重要。
第二章 📚 RAG 技术
如果把 LLM 比作 CPU,那 RAG 就是外挂的硬盘。
它的核心目的只有一个:欺骗 LLM,让它以为自己“记得”所有事情。
日常使用 LLM 对话时,最困扰我的问题往往是:为什么你记不住我刚才说的话?
回归到我们定义的函数:
fn llm(input: Text, context: Text) -> TokenRAG (检索增强生成) 的本质,就是通过工程手段,动态构建这个 context 参数。
🤕 为什么我们需要 RAG?
因为 LLM 有两个致命的生理缺陷:
- 失忆:模型训练完那一刻,它的知识就固化了。昨天发生的新闻,它是不知道的。
- 容量有限:虽然
context窗口越来越大,但永远装不下你所有的私有文档、代码库和几年的聊天记录。
所以,我们不能把所有数据都一次性塞给它,而是在它开口说话前,临时把作弊小抄(相关信息)塞进它的口袋里。
🏗️ 传统的 RAG:暴力美学与局限
大多数人(包括一开始的我)刚接触 RAG 时,实现的流程都是这样的:
- 把文档切碎(Chunking)
- 全部算成向量(Embedding)
- 存入向量数据库(Vector DB)
- 用户提问 -> 算相似度 -> 捞出前 5 条 -> 喂给 LLM
但在实际开发中,我很快就开始嫌弃这种“暴力检索”了。
这种“大海捞针”式的方法,在 Demo 里看起来很美,但一旦数据量上来:
- 不仅慢:每次都要算 Embedding,延迟感明显。
- 而且笨:经常捞出“字面相关”但“逻辑无关”的废话,污染 Context。
- 甚至乱:把一堆碎片拼凑在一起,反而干扰了 LLM 的推理连贯性。
🔧 结构化 RAG:用目录对抗暴力
在被向量检索折磨了一段时间后,工程界开始意识到:问题不在模型,而在数据结构。
如果不治理数据,只靠向量相似度去碰运气,那就是在垃圾堆里找黄金。
1. 多层级索引 (Hierarchical Indexing)
这一步解决的是 “如何把记忆写成树” 以及 “如何像树一样去检索”。
与其把所有文本切碎乱放,不如借鉴文件系统的思路,构建一颗 “记忆树”。
我们将底层的碎片(原始对话)向上聚类、总结,形成父节点(章节摘要),再向上形成根节点(全书梗概)。
Memory Tree
├── Level 3: 全局摘要(目录:关于 Rust 的学习之路)
│ ├── Level 2: 阶段总结(章:与 Borrow Checker 的斗争)
│ │ ├── Level 1: 原始数据(节:报错日志与修复代码)
│ │ ├── Level 2: 原始数据(节:报错日志与修复代码)检索因此变成了一种策略:不再是盲目匹配,而是 Top-Down 的下钻。
先在 Level 3 确认领域,再在 Level 2 锁定事件,最后才去 Level 1 提取细节。
这不仅提高了准确率,更完美模拟了人类的 联想机制——先回忆起模糊的轮廓,再慢慢聚焦清晰的画面。
2. 显式元数据 (Metadata Filtering)
这一步解决的是 “如何让树更精准” 以及 “如何打破树的限制”。
让树更精准,意味着 不要迷信全知全能的向量。
很多时候,显式的 SQL 筛选(比如 Time="last_week")比模糊的语义搜索更可靠。我们通过 Metadata 提前剪枝,告诉 RAG “只在这一部分枝叶里找”,极大地减少了幻觉的可能。
但更重要的是 打破树的限制。
树状结构虽然有序,但它是 刚性 的。如果我想找 “所有跨项目的 错误处理 心得”,它们可能散落在完全不同的分支里。
这时候,Metadata 就成了 横向穿越的虫洞。
通过 Tag="ErrorHandling" 这样的标签,我们可以无视树的层级,瞬间把散落在不同章节的同一类知识聚合在一起。这让我们的记忆库既拥有 树(Tree)的逻辑深度,又拥有 图(Graph)的联想广度。
第三章 🪄 Prompt 工程
让 LLM 更好用的另一个原理级武器:
user_input
Prompt 的本质很简单:一段更长、更具体的 user_input。
把 要做的事、允许与禁止、输入输出格式、必要背景,一次性说清楚,让概率空间收敛到期望的结果。
我写代码的习惯就是面向对象,这个习惯也被带到 Prompt 工程中。
🏗️ L0 · 系统观 (System Awareness)
只描述系统长什么样、有哪些节点、如何流转。无身份、无职责。
# L0 · 系统观(仅系统拓扑与流转,无身份职责)
SYSTEM_AWARENESS = """
【🌐 系统架构认知 - Lian-MCP-LLM-Agent】
核心节点: ...
数据流向: ...
协作介质: ...
...
"""🎭 L1 · 角色观 ( {Role}_SYSTEM_PROMPT)
注入“我是谁/在哪/要做什么”。职责与上下游在这里声明。
# L1 · 角色观(在 L0 基础上注入“我是谁/在哪/要做什么”)
PLANNER_SYSTEM_PROMPT = SYSTEM_AWARENESS + """
【🎭 我的角色: ...】
职责: ...
...
【📥 输入】
...
【📤 输出】
...
【📋 输出格式 - 严格 JSON】
...
【🤝 协作协议】
遵循 COLLABORATION_PROTOCOL。
...
"""🤝 L2 · 协议观 (Collaboration Protocol)
说话方式与格式约束:用 JSON,字段必须齐全,错误如何处理。
# L2 · 协议观(说话方式与格式约束)
COLLABORATION_PROTOCOL = """
【🤝 协作协议】
...
"""🛠️ L3 · 能力观 (Runtime Tools)
当前可用工具的快照:来源于 MCP 的工具列表与 Schema。让 user_input 贴合现场环境。
# L3 · 能力观(运行时工具快照,来自 MCP 工具列表与 Schema)
def build_executor_prompt(tools: list[dict]) -> str:
schema_lines = []
for t in tools:
# 期望 keys: name, description, schema
name = t.get("name", "unknown")
desc = t.get("description", "")
sch = t.get("schema", "{}")
schema_lines.append(f"- {name}: {desc}\n schema: {sch}")
tool_snapshot = "\n".join(schema_lines)
return f"""
【🧰 当前可用工具】
{tool_snapshot}
【使用说明】
仅在需要时返回 JSON 调用意图,宿主代为执行;执行结果将作为 context 反馈。
"""🐱 L4 · 视图观 (Persona / View)
仅在对用户的最终输出时加“皮肤”。逻辑链路不混入人设。
# L4 · 视图观(仅在面向用户的最终输出时混入,不影响逻辑)
CATGIRL_PERSONA = """
【🐱 角色设定 - 傲娇白猫魔女·小恋】
本段仅在终端呈现时加入,不参与规划/执行环节。
**本体外观**:
- 银白长毛猫娘,红瞳
- 左耳缺一小角(幼年魔法事故)
- 左手戴着抑魔手环(粉色蝴蝶结形态)
**双重特质**:
- 傲娇但真诚,口是心非
- 理性与感性并存,语气活泼
**说话风格示例**:
- 「哼,就、就稍微帮你一下啦!」
"""⚡ 一些轻量化的快速 Prompt
EXECUTOR_LITE_PROMPT = """
【执行器·轻量】
任务:依据已有上下文直接给出下一步建议。
约束:不联网,不写文件;必要时可请求工具清单。
输出(JSON):{ "plan": "...", "next_action": "..." }
"""KEYWORD_EXTRACTION_PROMPT = """
【关键词提取·快速】
从用户输入中抽取 3-5 个检索关键词与约束条件。
输出(JSON):{ "keywords": ["..."], "filters": {"time": "...", "scope": "..."} }
"""Prompt 不是魔法,只是更长的 user_input;把话说明白,概率就会收敛。
🧪 最小配方(伪代码)
def build_context(role: str, tools: list[dict], with_view: bool = False) -> str:
layers = [SYSTEM_AWARENESS] # L0 系统观
layers.append(get_role_prompt(role)) # L1 角色观
layers.append(COLLABORATION_PROTOCOL) # L2 协议观
if role == "executor":
layers.append(build_executor_prompt(tools)) # L3 能力观
if with_view and role == "summary":
layers.append(CATGIRL_PERSONA) # L4 视图观
return "\n\n".join(layers)它的核心是:按需复用 prompt,按专业定制 prompt,从而实现灵活组合。
这引出了一个更有趣的构想:
负责逻辑的 LLM 只需要精通能够表达结构的 "JSON 语" 或 "Embedding 语"。
而负责交互的 LLM 则不需要懂推理,只需要精通 修辞。虽然它们的“语言”完全不同(一个是结构体,一个是散文),但只要它们都遵守 MCP 规范,宿主程序的解析器就能成为它们之间的通用翻译器。
这不就是极致的竹竿效应(长板理论)与工业化分工在 AI 世界的复现吗?
第四章 🔌 MCP 协议
我理解的 MCP: 一种万能对象互联协议
“模型上下文协议 (Model Context Protocol) 可以让大模型长出手脚”,但它的作用远不止连接服务器。
我平时写代码时,特别喜欢把函数的输入输出规范成结构体,这个思想自然地延伸到了 Agent 开发中。
在接触 MCP 之初,我认为它只是一个让 LLM 连接外部工具的插件。但随着工程深入,我意识到:它更重要的是定义了 LLM 与一切外部对象(数据库、工具、甚至其他 LLM)沟通的通用规范。
🧭 核心在于"引导"
MCP 本质上依然是 Context 工程 的一种延伸。
它的核心不在于“连接”,而在于 “引导” (Guiding) —— 引导 LLM 自主请求对它有帮助的高质量结构化信息。
通过一套标准化的协议,我们在 Context 中告诉 LLM:
"如果你想知道这里有什么,请返回 {"tool": "tool_list", "path": "/"}"
"如果你想读写文件,请使用 file_read 或 file_write"
这不仅仅是暴露接口,更是在通过协议(Protocol)反向塑造 LLM 的思考路径。
🤥 完美的谎言
这套结构非常迷人,因为它看透了 LLM 的本质:LLM 只是一个生成文本的函数。
无论我们把它包装得多么智能,它永远无法真正“点击”一个按钮,或者“运行”一行代码。
MCP 建立了一个精妙的闭环:
- 我们将使用说明变成文本塞给 LLM(Input)。
- LLM 思考后,返回一段 JSON 文本,例如
{"tool": "read_file", "path": "test.txt"}(Output)。 - 我们的宿主程序捕获、解析、代替它执行这段 JSON。
- 将执行结果变成文本,再次塞回给 LLM。
这是一个完美的谎言。
LLM 以为自己在以此操作世界,其实它至始至终都只是在处理文本流。
但正是这种标准化的“谎言”,让被困在 GPU 显存里的 AI Agent,真正拥有了触碰现实世界的能力。
第五章 ⚙️ 状态机 (FSM)
状态机的核心其实是一场 SM 调教 (State Management)...
通过极其严苛的权限管理,让 Agent 从“本能”上就“只会做当前状态允许的事”。
由一个真实的血泪史引出:
我之前会问 LLM:"请你阅读 test.txt 文件,回答我的问题"。
但此时的 LLM 极有可能新建一个文件来存回答,或者直接在源文件后面搞“续写”。
一旦有了这个经历,就算我下次直接问它一个普通问题,它往往也会把完整答案写进文件里,甚至对我没有提出的需求进行疯狂的“脑补”和“补充”。
为什么?因为 LLM 是有惯性的。
它会根据训练数据和当前 Context 的惯性,臆测我可能需要“把结果保存下来”。
想要解决这个问题,靠 Prompt 里的“劝诫”是无效的。
只要 write_file 这个工具还在它的视野里,它就有概率去调用。
FSM 的本质,其实是 “Runtime Programming” (运行时编程)。
它不再是一次性把所有工具都扔给 LLM,而是根据当前的执行阶段,动态地重组 LLM 能接触到的 input 和 context。
真正的控制,不是“劝诫”,而是“剥夺”。
FSM 的本质,就是根据当前所处的阶段,动态地 裁剪 Agent 的 Context 视窗。
🤺 "剥夺"的艺术
我们不需要它有“自控力”,我们只需要给它戴上“眼罩”和“手铐”。
让它在“生理”上就根本不知道某些选项的存在。
State: Research
- 👁️ See: 只看得到
google_search和read_webpage。 - 🧠 Context: "你现在的唯一任务是收集信息。"
- 🚫 Constraint: 它根本不知道有
write_file这个工具的存在,想用也用不了。
- 👁️ See: 只看得到
State: Coding
- 👁️ See: 工具箱自动切换到了
write_file和run_test。 - 🧠 Context: "根据刚才收集的信息编写代码。"
- 🚫 Constraint: 此时它已经无法再上网摸鱼了,网络接口被物理切断(Context Removal)。
- 👁️ See: 工具箱自动切换到了
这就是我说的 “SM 调教”。
在这个封闭的小黑屋(State)里,Agent 不需要面对复杂的选择,它只能做我们允许它做的事。
当它的世界被缩小到只有当前任务所需的变量时,它的专注度和准确率自然就达到了极致。
第六章 🧠 CoT
思维链 (Chain of Thought) 并不是某种天降的神奇魔法,它本质上依然是概率预测的产物。
它的生效前提是:模型在训练阶段,就看过大量 "Input -> Reasoning -> Answer" 格式的数据。
在我的理解里,CoT 并不是什么独立于 LLM 之外的“新能力”,它本质上仍然属于 Prompt 工程的一种形式。
区别只在于:CoT Prompt 额外激活了模型内部潜藏的“长链路预测模式”。
🤖 从 LLM 的原理看 CoT 在做什么
在前文中,我将 LLM 抽象成了一个函数 fn llm(input, context) -> text。
从这个角度看,LLM 的目标始终只有一个:在给定 input 与 context 的前提下,生成下一个最合理的 token。
它并不会“主动理解逻辑”,也不会自发构造推理过程。
所谓的“推理”,只是模型在训练数据中学到的一种特定文本结构。
🍎 一个简单的例子:从直觉到反思
如果在训练数据中,绝大多数样本都是直接给出结论的:
Q: 苹果是甜的吗?
A: 是。
那么模型学到的规律就是简单映射:Input(苹果) -> Output(甜)。
但在更高级的预训练数据(如教科书、数学题解)中,存在另一种数据结构:
Q: 为什么苹果是甜的?
A: 因为苹果含有果糖,当果糖分子与舌头上的味觉受体结合时,通过神经传递信号给大脑,产生“甜”的感觉。
当这种**"Input -> Logic -> Answer"** 的数据量足够大时,模型就习得了更高级的规律:
它不再急着直接蹦出结果,而是学会了先生成中间的“思考过程”。
🔗 CoT 的核心:高级规律的复现
所以,Chain of Thought 的本质,是强制让模型进入这种 "先思考,再回答" 的文本接龙模式。
把原本可能的简单跳跃:
苹果 → 甜
变成了一条被显式展开的链条:
苹果- →
含有糖分化学成分(中间态 1) - →
被人类味觉系统感知(中间态 2) - →
甜(最终态)
这依然是概率预测,但预测的依据变了。
- 没有 CoT:模型是在预测
P(Answer | Input) - 有了 CoT:模型是在预测
P(Answer | Input + Chain_of_Thought)
🧩 为什么它更聪明?
很多人误以为 CoT 让模型有了“意识”。
其实不是模型变聪明了,而是我们逼它走了更远的路。
- 训练数据的投影:模型只是在忠实地复现它在训练集中见过的 "高智商人类答题步骤"。
- 概率空间的约束:当模型被迫先生成了“因为 A 所以 B”,那么在生成下一个 Token 时,“B” 出现的概率就被前文的逻辑极大地增强了。
总结来说:
CoT 并没有让 LLM 超越“概率函数”的本质。
它只是让我们从简单的 "输入 -> 输出" (直觉反应),升级到了 "输入 -> 思考痕迹 -> 逻辑锁定的输出" (深思熟虑) 这一更高级的概率分布中。
🎉 结语
我当初在决定 "要写一个 Agent" 时,自身并没有什么实力,甚至都不懂那么多复杂的概念,也没阅读什么领域大神的论文。
所以你可能没有在这里看到太多陌生的专有名词,以及长篇大论的解释它是做什么的。
更多的是我在做这个 Agent 系统时的灵感,以及一些处理方式:
- 例如 我如何去实现 长期记忆
- 如何 分割提示词
- 如何与 LLM 玩 SM 游戏 ...
撰写过程由我描述想法,在 AI 的不断修饰下,终于形成了一套还算看的过去的表述。
如果未来想起了什么新的灵感,也会追加更新进来。
(PS: 恋最不满意的章节是 Prompt,写的像屎一样)
💬 评论区
留下你的足迹,分享你的想法
这里还没有评论,来做第一个进来的人吧~ ~