多智能体系统上下文工程-超越提示词-构建上下文与推理的透明架构引擎-一-
多智能体系统上下文工程:超越提示词,构建上下文与推理的透明架构引擎(一)
原文:Context Engineering for Multi-Agent Systems
译者:飞龙
多智能体系统上下文工程:超越提示词,构建上下文与推理的透明架构引擎
-
组合总监:Gebin George
-
关系负责人:Tanya Cruz
-
项目经理:Prakajta Naik
-
档工程师:Tanya Cruz
-
技术编辑:Rahul Limbacchi
-
文字编辑:Safis Editing
-
索引编员:Pratik Shirodkar
-
校对员:Tanya Cruz
-
制作设计:Shantanu Zagade
我想将这本书献给我的家人朋友,他们是我快乐的来源。
— Denis Rothman
贡献者
关于作者
Denis Rothman 毕业于索邦大学和巴黎-西德罗大学。他开创了最早获得专利的 word2matrix 嵌入算法和 AI 驱动的对话代理之一。在职业生涯早期,Denis 开发了一款认知 NLP 聊天机器人,被酩亚洲(Moët & Chandon)等其他全球品牌作为自动化语言训练器采用。随后,他为空客(Airbus,原法国航空航天公司)创建了 AI 资源优化器,并由 IBM 和服装行业领导者实施。他在全球范围内使用的高级计划与调度(APS)解决方案,塑造了各行业的供应链智能。
通过他的书籍,Denis 与全球致力于有目的地塑造 AI 的思考者、构建者和学习者社区分享他的创新经验。
关于评审员
Paras Patel 是一位平台工程负责人,拥有超过 14 年的硅谷经验,涵盖云计算、DevOps 和 SRE。在乐天(Rakuten),他领导规模化创新,构建并管理服务数百万用户的 Kubernetes 平台,同时推动整个机构的 AI 转型计划。Paras 以在可观测性、Kafka、Redis 和 Elasticsearch 方面的深厚知识而闻名,专注于设计生产级 AI 基础设施,以弥合前沿技术与应用之间的差距。
第 5 章:加固上下文引擎 127
第 6 章:构建用于上下文缩减的摘要代理 163
第 7 章:高保真 RAG 与防御:受 NASA 启发的科研助手 195
-
准备源文档 • 201
-
更新数据加载和处理逻辑 • 202
-
验证 • 204
-
第 2 部分:升级上下文引擎的能力 • 205
-
实现
helper_sanitize_input函数 • 205 -
高保真研究者应用 • 207
-
-
第 3 部分:最终应用:NASA 研究助手 • 209
-
控制台 • 210
-
解构高保真追踪和输出 • 211
-
上下文引擎的验证与向后兼容 • 212
-
上下文引擎完整清单 • 213
-
主应用笔记本函数 • 213
-
辅助函数 (
helpers.py) • 214 -
代理 (
agents.py) • 215 -
注册表 (
registry.py) • 216 -
引擎 (
engine.py) • 217
-
-
上下文引擎 • 219
-
整体架构 • 220
-
第 8 章:面向现实架构:审核、延迟与策略驱动的 AI
第 9 章:面向品牌与敏捷性架构:战略营销引擎 271
第 10 章:生产级 AI 蓝图 295
-
基础设施与容器化 • 303
-
防御数据流水线免受投毒和对抗性攻击 • 306
-
通过自动化护栏确保合规性与安全性 • 307
-
通过创新的工作流执行治理与质量 • 307
展示业务价值 308
-
从成本中心到价值倍增器 • 309
-
通过可验证性和安全性建立利益者信任 • 312
-
创建战略资产 • 312
总结 314
问题 315
参考文献 316
进阶阅读 316
第 11 章:解锁您的专属福利 317
通过 3 个简单步骤解锁本书免费福利 317
附录 A:上下文引擎(Context Engine)参考指南 321
理论基础 322
语义蓝图 322
-
玻璃盒架构的优势 • 323
-
系统架构与工作流 • 323
第 0 阶段:数据接入流水线 324
-
上下文引擎工作流 • 325
-
通用库(
commons library)参考 • 326-
文件:
helpers.py• 326 -
文件:
agents.py• 330 -
文件:
registry.py• 333 -
文件:
engine.py• 334 -
模块:
utils.py• 335
-
数据接入流水线 338
-
流水线步骤 • 338
-
接入上下文库 • 339
执行与运行 339
-
生产环境防护:审核、清洗与策略 341
-
输入清洗(提示词注入防御)• 341
-
两阶段内容审核协议 • 341
-
-
自动化的局限与策略的角色 342
- 策略驱动的解决方案 • 342
-
运行现实:延迟与随机性 343
附录 B:答案 345
-
第 1 章 345
-
第 2 章 346
-
第 3 章 347
-
第 4 章 348
-
第 5 章 349
-
第 6 章 350
-
第 7 章 352
-
第 8 章 353
-
第 9 章 354
-
第 10 章 359
-
为什么要订阅? 359
你可能喜欢的其他书籍 360
前言
生成式 AI 强大但不可预测。本书将向你展示如何通过超越简单的提示词调优(prompt tinkering)并像架构师一样思考,将这种不可预测性转化为可靠性。其核心是新兴的学科——上下文工程(context engineering),即对结构化、管理和治理大语言模型用于推理、决策和生成的信息的实践。你将通过“上下文引擎”(Context Engine)探索这一概念,这是一个构建在多智能体协作和检索基础上的透明玻璃盒系统。你将学习如何一步一步加强并部署这一架构,将原始模型输出转换为可验证且符合策略的内容。
在整个章节中,你将通过第一原理构建上下文引擎,从上下文设计和语义蓝图开始,通过模型上下文协议(MCP)编排智能体。随着引擎的成熟,你将集成内存、检索和防护,针对数据投毒和提示词注入引入审核层,以确保符合策略标准。在结束时,你将拥有一个生产级 AI 的蓝图,上下文引擎将成为黑盒提示词与可靠性之间的桥梁,并在不同领域中复用。
本书适合谁
本书适合 AI 工程师、软件开发人员、系统架构师以及数据科学家,他们希望超越单纯的提示词生成,学习设计结构化、透明且感知上下文的 AI 系统。它也吸引对大语言模型(LLMs)有了解机器学习工程师和解决方案架构师,他们渴望学习编排智能体、集成内存与检索并执行防护。在结束时,读者将掌握构建可适配、可验证架构的技能。
本书涵盖内容
第 1 章,从提示词到上下文:构建语义蓝图。 介绍了上下文工程的原理,并展示了结构化上下文、语义蓝图和智能体编排如何将基于提示词的不可预测性转化为目标驱动的系统。它建立了构建透明多智能体架构的基础,内存、检索和防护将在书中演变为生产级上下文引擎。
第 2 章,通过 MCP 构建多智能体系统。 上下文工程从单智能体控制扩展到多智能体协作,展示了专业智能体如何通过模型上下文协议(MCP)协作完成复杂的多步骤工作流。它演示了编排器、智能体和验证器如何通过结构化上下文进行通信,以确保稳健多智能体系统中的可靠性、错误恢复和事实准确性。
第 3 章,构建感知上下文的多智能体系统。 扩展架构以实现事实检索与程序指令分离,使智能体能够利用知识和风格上下文进行推理和写作。它引入了上下文管理员(Context Librarian)和研究员(Researcher)智能体,通过 MCP 进行编排,动态检索语义蓝图和事实数据——为演进中的上下文引擎奠定自适应生成的基础。
第 4 章,组装上下文引擎。 将上下文工程的原理整合为完整的自主架构,利用专业智能体进行规划、执行和反思。它引入了规划器(Planner)、执行器(Executor)和追踪器(Tracer)模块,通过模型上下文协议集成,创建一个上下文驱动的推理系统。
第 5 章,加固上下文引擎。 应用模块化、依赖注入和结构化日志等工程原理,将实验性的上下文引擎转换为生产级系统。它详细介绍了如何将原型重构为独立的、可测试的组件——helpers、agents、registry 和 engine,创建出适用于真实世界部署的架构。
前言
第 7 章,高保真 RAG 与防御:受 NASA 启发的研究助手。 为上下文引擎(Context Engine)升级了可验证性和安全性,引入了高保真检索流水线,为每个事实附加了源元数据,并实现了基于引用的推理。它还通过输入清理实现了针对数据投毒和提示词注入的防御层,在上下文工程的多智能体架构中建立了企业级的信任、可追溯性和向后兼容性。
第 8 章,面向现实的架构:审核、延迟与策略驱动的 AI。 通过引入两阶段审核、延迟预算和针对真实部署的策略安全措施,将上下文引擎从控制台原型转变为企业级系统。它通过“法律助手”这一用例演示了如何集成审核网关、策略执行和人回路(human-in-the-loop)治理,证明了可靠的上下文工程需要的不是代码级的防护,而是组织设计和策略对齐。
第 9 章,面向品牌与敏捷性的架构:战略营销引擎。 通过在不改变核心逻辑的情况下,将同一多智能体架构从法律合规助手重新任务化战略营销引擎,展示了上下文引擎的领域无关性。它引导读者通过营销知识库执行品牌一致性检查、进行竞争分析并合成具有说服力的内容,证明了上下文工程实现了 AI 与业务目标之间的模块化复用和跨领域适应性。
第 10 章,生产级 AI 的蓝图。 提供了将“白箱”上下文引擎部署为可扩展企业服务的框架,详细说明了如何通过容器化、编排、环境配置、异步执行和可观测性来实现生产化。它将成本管理、可验证检索、数据清洗和审核整合在一个统一的操作蓝图中——然后演示了这些工程模式如何通过可衡量的投资回报率(ROI)和合规保证转化为信任、治理和长期业务价值。
附录:上下文引擎参考指南 作为读者的技术伴侣,将书中介绍的所有架构概念、智能体工作流整合成一个单一的实用的实施指南。它为构建和维护作为企业级框架的上下文引擎提供了详细的参考。
充分利用本书的价值
如果你是 LLM 的初学者,在本书进入更高级的架构工作之前,第 1 和第 2 章将为你建立必要的概念和实践基础。
前言
为了确保一切运行顺利,请在深入代码之前设置好开发环境。每个实操章节都使用了可复现的基于 Python 环境。示例主要在 Google Colab 和 VS Code 中开发,确保了跨平台的灵活性。
在开始之前,请确保你拥有:
-
Python 3.10 或更高版本
-
配置了
openai、pinecone-client、tiktoken、tenacity和fastapi的 Google Colab 或本地环境 -
用于存储项目文件的 GitHub 或本地目录结构(包含
helpers.py、agents.py、registry.py、engine.py以及每个章节的 notebook 文件) -
OpenAI 账号(用于模型访问和审核)
-
Pinecone(用于向量数据库存储和检索)
-
可选:Google Cloud 或 AWS(用于第 10 章中的部署章节)
从第 5 章开始,我们开始使用依赖之前 notebook 的模块组件。在继续之前,确保你的环境配置正确,因为设置步骤可能不会每个章节中详细重复。
你的系统不需要高端,但满足以下硬件基准将帮助你避免性能问题:
-
最低:双核 CPU,8 GB RAM(用于本地运行)。
-
推荐:至少 16 GB RAM 的系统或云运行时(Google Colab Pro 或同等)。
-
GPU 加速是可选的,但对于嵌入生成和 token 密集型任务很有用。
如果你在本地运行,在尝试大上下文时请注意 token 和 API 成本。第 6 章专门引入了 Summarizer 智能体来某种程度上帮助管理这些成本。
在开始构建之前,创建一个专门的工作区以保持你的辅助脚本和 notebook 的整洁。熟悉检索工作流(RAG)和智能体编排(MCP),这些将是几乎所有章节的基础。需要时查阅附录:上下文引擎参考指南;它整合了每个章节的结构代码和组件说明,以便快速查询。
下载示例代码文件
本书的代码包托管在 GitHub 上,地址为 https://github.com/Denis2054/Context-Engineering-for-Multi-Agent-Systems。我们在丰富的书籍和视频目录中还提供了其他代码包,地址见 https://github.com/PacktPublishing。快去看看吧!
下载彩色图片
我们还提供了一个包含书中使用的截图/图表彩色图像的 PDF 文件。你可以在此处下载: https://packt.link/gbp/978106690053 。
使用的规范
本书中使用了多种文本规范。
CodeInText:表示文本中的代码词、数据库表、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“count_tokens 工具提供了测量值,而 Summarizer 智能体提供了操作。”
代码块设置如下:
class AgentRegistry:
def __init__(self):
self.registry = {
"Librarian": agents.agent_context_librarian,
"Researcher": agents.agent_researcher,
"Writer": agents.agent_writer,
# NEW: Add the Summarizer Agent ---
"Summarizer": agents.agent_summarizer,
}
任何命令行输入或输出如下所示:
Prepared 3 context blueprints.
Bold(粗体): 表示新术语、重要词汇或你在屏幕上看到的词。例如,两种类型都由 embedding model 处理。
警告或重要注释显示如下所示。
提示和技巧显示如下所示。
联系我们
我们随时欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问或任何一般反馈,请发送邮件至 customercare@packt.com 并注明书名。
错误:尽管我们尽力确保内容的准确性,但错误在所免。如果你在书中发现错误,我们将感谢你告知我们。请访问 http://www.packt.com/submit-errata ,点击提交错误并填写表格。
盗版:如果你在互联网上任何形式发现我们作品的非法拷贝版,我们将感谢你提供位置地址或网站名称。请联系 copyright@packt.com 并附上链接。
如果你兴趣成为作者:如果你对某个领域有专长并兴趣编写或为书籍贡献,请访问 http://authors.packt.com/
加入我们的 Discord 和 Reddit 社区
你并不是唯一在碎片化工具、不断更新和不明确的最佳实践中挣扎的人。加入一个不断发展的专业人士社区,交流未记录在文档中的见解。
| 加入我们的 Discord:https://packt.link/zivB | 在 Reddit 上关注我们:https://packt.link/@rExL |
| --- | --- |
| 或扫描下方二维码: | 或扫描下方二维码: |
| [QR CODE] | [QR CODE] |
分享你的想法
当你读完《Context Engineering for Multi-Agent Systems》后,我们很想听听你的想法!扫描下方的二维码直接进入本书的亚马逊评论页面并分享你的反馈。
https://packt.link/r/1806690047
你的评论对我们以及技术社区都至关重要,并将帮助我们确保提供高质量的内容。
您的书籍提供的免费福利
本书附带了免费福利以支持您的学习。现在激活这些福利即可立即访问(指令请参阅“如何解锁”章节)。
以下是您购买后可以立即解锁的内容简要概述:
| PDF 和 ePub 副本 | 下一代 Web 端阅读器 |
| :--- | :--- |
-
多设备进度同步:在任何设备上从上次离开的地方继续。
-
访问本书的无 DRM(数字版权管理)PDF 副本,可在任何地方、任何设备上阅读。
-
高亮和做笔记:捕捉想法,将阅读转化为持久的知识。
-
使用您喜爱的电子阅读器使用无 DRM 的 ePub 版本。
-
书签:在需要时保存并重新查看关键章节。
-
深色模式:切换到深色或深褐色色主题以减轻眼睛疲劳。
如何解锁
扫描二维码 码(或访问 packtpub.com/unlock)。按名称搜索此书,确认版本,然后按照页面上的步骤操作。
注意:请妥善保存发票。直接从 Packt 购买的不需要发票。
1
从提示词到上下文:构建语义蓝图
上下文工程(Context engineering)是将将生成式 AI 从不可预测的协作者转变为完全控制的创意伙伴的学科。提示词(Prompt)开启了随机机会的大门,而上下文(Context)则为可预测的结果提供了结构化的蓝图。这是从要求大语言模型(LLM)继续序列到构建一个闭环环境并接受不可预测性的根本性转变。这种演变让交互超越了简单的请求,进入了引导创作的领域,不仅告诉模型要做什么,还告诉它在您定义的边界内如何思考。
长期以来,我们一直把生成式 AI 视作神谕,向虚空发送提示词并希望获得一个连贯的回复。我们赞美它的高光时刻,却忽视了它的不一致性,并将不可预测性视为体验的一部分。但这是创作的艺术。本章不是关于如何提更好的问题,而是关于提供更好的计划并告诉 LLM 该做什么。
我们的旅程从一个实操演示开始,该演示通过五个级别的上下文复杂性,展示了每一层如何将输出从随机猜测转化为结构化的、目标驱动的响应。然后,我们从词汇的线性序列转向多维结构,引入语义角色标注(SRL),这是一种语言学技术,用于揭示谁对谁做了什么、在何时以及为什么这么做。以 SRL 为基础,我们构建了一个 Python 程序,将这些结构可视化为语义蓝图。最后,我们在完整的会议分析用例中综合这些技能,在用例中我们将引入上下文链(context chaining),并演示多步工作流如何将原始转录转换为洞察、决策和专业行动。
到本章结束时,你将不再数字荒野中寻找答案。你将成为那片荒野的建筑师,能够设计 AI 模型思维的格局,并引导它走向你选择的任何目的地。
本章涵盖了以下主题:
-
通过五个级别的上下文工程来构建语义蓝图
-
通过 SRL 从线性文本过渡多维语义结构
-
构建 Python 程序使用 SRL 对文本进行解析和结构化
-
应用上下文链作为逐步步进、受控推理的方法
-
使用完整的分析用例将原始转录转换为专业的电子邮件
理解上下文工程
上下文工程是控制和引导大语言模型(LLM)所学的信息世界的艺术与科学。它将你的角色从提问者转变为自信的导演。这就像给演员单句台词与给他们完整剧本(包括角色动机和舞台指令)之间的区别。你不再是在请求一场表演;你是在设计它,并告诉 LLM 该做什么。
理解上下文工程最好的方法是体验它。我们将跳过理论,从实操演示开始。这段旅程旨在让你体验这种感觉,让随机猜测演化为细致的、可靠的对齐的响应。在这里,我们将通过五个复杂性级别,如图 所示(Google, Microsoft, OpenAI):
-
1 级:基础提示词(无上下文):这是没有背景的简单指令。
-
2 级:更好的上下文(线性上下文):这是向后迈的一小步。添加线性线程改进了准确性,但缺乏风格或目的。
-
3 级:良好的上下文(目标导向上下文):这是上下文第一个里程碑。通过赋予模型明确的目标,其响应变得具有意图。
-
4 级:高级上下文(基于角色的上下文):这比仅有提示词更有结构化。通过分配角色,模型可以遵循动机。
-
5 级:语义蓝图:这是终极上下文。精确的计划转化为可重复的过程。
从提示词到上下文:构建语义蓝图
注意:你获得的响应可能会由于生成式 AI 的随机性而异。然而,设计良好的上下文总是优于即兴的提示词。
让我们从基础提示词开始。
1 级:基础提示词(无上下文)
基础提示词没有背景。AI 根据训练数据进行猜测,正如本例所示。我们的输入如下:
完成这个句子:“猫在玩……”
Gemini 2.5 的响应是:
...毛球。
以下是完成句子的几种方式:
...老鼠。
...绳。
...纸箱。
...激光笔点。
...小猫。
你可以看到,由于上下文,AI 会给出最可预测且单调的完成。
Microsoft Copilot 的响应是:
猫在玩毛球,直到它散落在地板上。
...在沙发下发现的一张皱的收据,像对待它是尘埃王国里最珍贵的宝贝一样拨弄它。
在这种情况下,输出是有创意的,但如果我们想要精确的响应,我们会认为这种响应是幻觉。没有上下文,AI 会默认给出它学习过的最可预测的完成。
OpenAI GPT-5 的响应是:
猫在玩毛球,直到它散落在地板上。
输出是陈词滥调。没有上下文,AI 会默认给出最可预测的完成。所有三个模型都可以说:
我抱歉,没有上下文,我无法正确完成这个句子。
我们可以看到,基础提示词会导致基础、不精确的响应甚至是幻觉。目标不是尝试通过模型超参数(如温度)寻找绕过方法,而是构建更好的上下文。让我们进入下一个。
2 级:更好的上下文(线性上下文)
在这里,我们添加了一个简单的前句作为添加上下文,这将提供线性的信息线索,提高了准确性,但没有引导 AI 的风格或目的。我们的输入如下:
完成这个句子:球掉在了地上,滚了几下。猫接着在玩……
Gemini 2.5 的响应是:
球掉在了地上,滚了几下。猫接着在玩球。
注意,该响应是事实且正确的,但缺乏创造力,这在没有更多上下文的情况下是正常的。
Microsoft Copilot 的响应是:
...球,用快速的小轻击拨弄它,并在房间里追逐它,就像它是世界上最令人兴奋的猎物一样。
OpenAI GPT-5 的回答如下:
球掉在了地板上,向前滚了几个英尺。猫咪开始玩球,用爪子来回地拨弄着。
在这里,回答在事实层面是正确的,而且具有创造性,但同样,它并不知道该生成什么,继续在随机发挥。
除了产生基础概率之外,这些回答并没有特别说服力,但它们相对准确。这对于简单任务可能有效,但当我们我们需要生成式 AI 模型执行任务时就失效了。那么,让我们进入构建上下文并设定目标。
第 3 级:良好的上下文(目标导向上下文)
这一级是上下文工程的第一步。我们将提供清晰且更丰富的描述细节,使大语言模型(LLM)能够根据我们的意图共同创建一个场景。
- 目标: 为儿童故事写一个简短的悬疑场景。
完成这个句子:
“古老的祖父钟敲响午夜,它的影子像爪子一样伸展。一个红球似乎在自己移动,从落满灰的单手椅上滚了出来。猫,尼克斯(Onyx)伏下身体,眼睛闪烁着光。它在玩……”
这里是 Gemini 2 的回答:
古老的祖父钟敲响午夜,它的影子像爪子一样伸展。一个红球似乎在自己移动,从落满灰的单手椅上滚了出来。猫,尼克斯伏下身体,眼睛闪烁着光。它在玩……一个微小的、闪烁的光影跳了舞,带着红球跑而去。球不仅仅是在滚动,它在木地板上跳动着,仿佛活了一样,像一只小红老鼠被拉到了窗户厚重的丝绒窗帘下。尼克斯发出了一声低沉的咆哮,那是它玩耍时发出的声音。球无声息地滑到厚实的深色布料后面。线头在空中悬停了一秒,紧绷并闷化了声音,随后也被拉入了黑暗。一个比叶子沙沙声响更大的微小细语从窗帘后传出:“我的。”
请看,这个回答更有针对性。我们开始得到我们想要的东西。记住,LLM 像人类一样,需要上下文来提供更有效的回答。拥有良好长上下文窗口的人类和 LLM 会提供更好的结果。
Microsoft Copilot 的回答如下:
...球只停留了一会儿就冻结了。球停了——完全纹止不动——但尼克斯的毛竖起来了。从单手椅后面传来轻微的吱呀声,像是地板板在低语。尼克斯向后退去,尾巴蓬起,眼睛盯着黑暗。这时,一声细微的笑声在房间里回荡。不是人类的笑声。窗户关着,窗帘在飘动。尼克斯嘶了一声,球又滚了起来——这次朝走廊滚去,留下一道淡淡的灰色灰尘。钟的滴答声现在变响了,每一秒都像鼓点一样。就在走廊吞没球之前,一只苍白的手从阴影中伸出来,抓住了它。尼克斯逃了。但那声笑声依然存在。
注意,在这种情况下,Microsoft Copilot 正好从我们输入的结尾处开始。此外,我们可以观察到,我们正在从提示词(非结构化上下文)向引导性请求转变。我们是舞者。
这是 OpenAI GPT-5 的回答:
...球,在木地板上来回拨弄。但球并没有停止,而是向他弹回——仿佛被一只无形的手推着。尼克斯僵住了,尾巴竖了起来,而钟的滴答声比任何时候都响,每一声滴答都在空旷的房间里回荡。
在这种情况下,回答非常好,并且完全从我们要求它的地方开始。总的来说,这些回答比第 2 级的更有创意且更有意。然而,它们仍然是松散的。为了实现更高的精确度,我们需要在上下文本身中引入结构。
第 4 级:高级上下文(基于角色的上下文)
我们不再让 AI 去推断戏剧性结构,而是现在定义每个参与者的角色。通过为 LLM 提供角色、物体和关系,我们赋予了它冲突和动机的框架。这使得回答在叙事上变得智能。我们的输入如下:
- 目标: 写一个悬疑的句子来延续儿童故事。
背景:
-
主角:尼克斯,一只勇敢的黑猫。
-
反派:一个古老的、闹鬼的祖父钟。
-
催化剂物体:一个神秘的红球。
-
环境:午夜一个昏暗、灰尘土的房间。
-
前情:钟响了,球滚了出来。
从这里继续写故事:“黑猫尼克斯伏下身体……它接着在玩……”
这里是 Gemini 2.5 的回答:
神秘的红球,轻轻地拨弄它,直到一声低沉的呻吟从钟和它的尾巴中传出,玻璃门打开,露出了内部的黑暗。
这,它并不介意询问。准确地告诉 LLM 我们想要什么,结果是可以接受的且更有针对性。
Microsoft Copilot 的回答如下:
球,直到它闪烁着奇异的光芒,钟的指针疯狂地向后旋转,释放出一阵不寒而然的低语,顺着地板向他爬来。
在这种情况下,LLM 注意到了我们已经变成的导演。故事遵循了我们的结构化指令。
这里是 OpenAI GPT-5 的回答:
黑猫尼克斯伏下身体……它接着在玩红球——结果当祖父钟的钟摆自行发出吱吱声时,它僵住了,在午夜的寂静中摆得越比越快。
在这种情况下,回答遵循了我们上下文的结构。由此,我们已经从无引导的线性提示词转向结构化的上下文。我们不再是在提问,而是在指令——模型也是如此的。
第 5 级:语义蓝图
这一级代表了上下文架构的完整实现。在这里,我们通过结构化的格式为模型提供精确且无歧的计划,使创作行为成为一种可靠的过程。这被称为语义蓝图,它是通过语义角色来引导的:场景目标、参与者、描述、待完成的动作、代理(执行动作的人)以及病人(受动作影响的人)。
我们的输入如下:
任务:生成一个悬疑的句子。
语义蓝图:
{
"场景目标": "通过反抗增加张力",
"参与者": [
{ "name": "尼克斯", "role": "代理", "description": "黑猫"},
{ "name": "红球", "role": "病人", "description": "神秘的"},
{ "name": "祖父钟", "role": "威胁来源", "description": "古老的" }
],
"待完成的动作": {
"谓词": "玩",
"代理": "尼克斯",
"病人": "红球"
}
}
待完成句子:“它接着在玩……”
Gemini 2.5 的回答如下:
它接着在玩红球,在祖父钟的影子下缓慢地拨着它,沉重的钟摆开始了不可逆转的摆动。
输出完全遵循了蓝图。我们不再是即兴;模型正在执行。
回答是:
它接着在玩红球,它的影子在祖父钟逼人的滴答声下挑地闪烁,仿佛在挑战时间。我们提供的结构贯穿了叙事。
OpenAI GPT-5 的回答如下:
它接着在玩红球,随着尼克斯爪子每一次大胆的挥动,祖父钟持续的滴答声似乎变得越来越响。
等等,我们正在获得 LLM 需要做什么。
在这个阶段,我们不再是 LLM 即兴表演的观众。我们是导演,LLM 是根据我们的剧本表演的演员。但是,像第 5 级这样的语义蓝图是如何工作的?为了回答这个问题,我们转向语义角色标注(SRL),这种方法将带我们开启首次从语言序列到多维结构的旅程。
SRL:从线性序列到语义结构
提升我们对线性序列结构感知的关键是 SRL,这是一种强大的语言技术,最初由 Lucien Tesnières 形式化,随后由查尔斯·Charles J. Fillmore 完善。你可以在参考文献中找到他们作品的链接。SRL 在这些伟大的先驱者之后多年出现,其工作是解构线性句子以回答最基本的问题:谁对谁做了什么,在何时,为什么?它超越了简单的语法,识别每个组件在整体动作中所扮演的功能性角色。
注意
对于想要了解这些基础概念(如 SRL 和语义蓝图)如何融入上下文引擎架构的读者,附录提供了关于整个系统如何将这些概念结合在一起的简要概述。
考虑一个简单的句子:
Sarah 早上向董事会推介了新项目。
大语言模型(LLM)将词序列解释为标记 链。而上下文工程师使用语义角色结构标注(SRL)可以看到更多内容:一个 stemma(词干)或图结构,它将每个词映射到它的语义角色。核心动作是 pitched(推介),而其他所有组件都被分配了与该动作相关的角色。
通过对这些角色进行标记,我们将执行以下操作:
-
重构原本线性词汇字符串的多维语义结构
-
定义一个 LLM 可以遵循的语义蓝图,正如前面提到的第 5 级示例所示。
这一过程是高级上下文工程的基础技能。为了理解 SRL 过程,让我们编写一个 Python 程序,将这一强大的理论付诸实践。
在 Python 中构建 SRL 笔记本
这个 Python 脚本的目的是提取句子的核心部分并将它们转换为意义的视觉图表。与其让结构隐藏在文本中,该程序画出了词汇与角色之间关系的图像。图 1.2 展示了整个过程:
图 1.2:SRL 程序流程图
让我们逐步过程的每个阶段:
- 用户输入:当你调用主函数
visual_srl()时,之旅开始了。在这里,你提供了句子的构建块,例如动词(谓语)、执行者(执行动作的实体)、受者(接收或受动作影响的实体)以及用文本参数表示的其他语义角色。
第一章
数据结构化
主函数将这些组件组织成一个 Python 字典。每个条目都被分配了正确的 SRL 标签,因此最初松散的词汇列表变成了一个结构化的角色映射。
绘图引擎
一旦引擎就绪,它会被传递给内部辅助函数 _plot_stemma()。这个函数只做一件事:绘制。
画布设置
绘图引擎使用 Matplotlib 创建一个空白画布,为图表准备舞台。
动态定位
该函数计算每个角色节点的位置,以确保无论包含多少组件,布局都能保持清晰和平衡。
绘制 Stemma(图)
引擎随后将核心作为根节点,将每个角色作为子节点添加,并用带标签的箭头将它们连接起来。曾经的线性句子现在变成了意义的视觉地图。
最终显示
最后,函数添加标题并显示完成的蓝图。
到此为止,我们准备好在实践中实现 SRL 了。请参考 GitHub 仓库第 01 章中的 01.ipynb。
注意
关于代码本身:它是为了清晰和教学编写的,而不是为了生产环境。重点是尽可能直接地展示信息流。出于这个原因,笔记本避免了会分散体验的沉重控制结构或错误处理。这些精炼可以在以后构建生产系统时添加。此外,要在本地运行该笔记本,你需要安装 spaCy、Matplotlib 和 Graphviz,并使用命令
python -m spacy download en_core_web_sm下载 spaCy 的英文模型。
让我们开始用 Python 构建一个语义蓝图的可视化工具,它将生成我们的 stemma,这本质上是一个带有语义节点和边缘可视化的图。我们将逐块拆解脚本,解释每个部分的作用。
首先,导入必要的工具。我们的可视化器依赖 matplotlib 进行绘图:
-
matplotlib.pyplot as plt:主绘图接口,使用惯用的别名plt导入以便使用。 -
matplotlib.patches.FancyArrowPatch:用于绘制整洁的定向箭头的工具,将动词与其角色连接。
有了这些工具,我们现在可以定义 SRL 可视化函数了。
主函数:visual_srl
我们程序的核心是主函数visual_srl()。这是主要的交互点,由图 1.2中的步骤 1 所示。作为用户,你将句子的核心组件作为参数提供,函数负责处理其余工作,组织数据并将其发送给绘图辅助函数。
参数直接对应于之前定义的语义角色。为了保持接口的灵活性,函数还接受**kwargs,这允许你传递任意数量的可选修饰符(例如时间或位置细节),而不会使函数签名复杂化。
visual_srl()的目的是将这些角色汇成一个字典(srl1_roles),然后将谓语(动词)连同交给内部的_plot_stemma()进行可视化:
def visual_srl(verb, agent, patient, recipient=None, **kwargs):
"""
创建一个语义蓝图并将其可视化为 stemma。
这是主要面向用户的主函数。
"""
srl_roles = {
"Agent (ARG0)": agent,
"Patient (ARG1)": patient,
}
if recipient:
srl_roles["Recipient (ARG2)"] = recipient
# 添加其他修饰符
for key, value in kwargs.items():
# 格式化 key 为 ARGM-
role_name = f"{key.capitalize()} (ARGM-)"
srl_roles[role_name] = value
_plot_stemma(verb, srl_roles)
注意
该函数创建一个语义蓝图并将其可视化为 stemma,这是主要面向用户的主函数。
在开始绘制 stemma 之前,我们仔细查看这些定义的角色。
定义语义角色
考虑一个简单的句子:
Sarah 早上向董事会推介了新项目。
大语言模型将词序列解释为标记链。而上下文工程师使用 SRL 可以看到更多内容:一个stemma(词干)或图结构,它将每个词映射到它的语义角色。核心动作是 pitch(推介),而其他所有组件都被分配了与该动作相关的角色。
通过对这些角色进行标记,我们将执行以下操作:
-
重构原本线性词汇字符串的多维语义结构
-
定义一个 LLM 可以遵循的语义蓝图,正如前面提到的第 5 级示例所示
这一过程是高级上下文工程的基础技能。为了理解 SRL 过程,让我们编写一个 Python 程序,将这一强大的理论付诸实践。
在 Python 中构建 SRL 笔记本
这个 Python 脚本的目的是提取句子的核心部分并将它们转换为意义的视觉图表。与其让结构隐藏在文本中,该程序画出了词汇与角色之间关系的图像。图 1.2 展示了整个过程:
图 1.2:SRL 程序流程图
让我们逐步过程的每个阶段:
- 用户输入:当你调用主函数
visual_srl()时,之旅开始了。在这里,你提供了句子的构建块,例如动词(谓语)、执行者(执行动作的实体)、受者(接收或受动作影响的实体)以及用文本参数表示的其他语义角色。
第二章
-
数据结构化:主函数将这些组件组织成一个 Python 字典。每个条目都被分配了正确的 SRL 标签,因此最初松散的词汇列表变成了一个结构化的角色映射。
-
绘图引擎:一旦引擎就绪,它会被传递给内部辅助函数
_plot_stemma()。这个函数只做一件事:绘制。 -
画布设置:绘图引擎使用 Matplotlib 创建一个空白画布,为图表准备舞台。
-
动态定位:该函数计算每个角色节点的位置,以确保无论包含多少组件,布局都能保持清晰和平衡。
-
绘制 Stemma(图):引擎随后将核心作为根节点,将每个角色作为子节点添加,并用带标签的箭头将它们连接起来。曾经的线性句子现在变成了意义的视觉地图。
-
最终显示:最后,函数添加标题并显示完成的蓝图。
到此为止,我们准备好在实践中实现 SRL 了。请参考 GitHub 仓库第 01 章中的 01.ipynb。
注意
关于代码本身:它是为了清晰和教学编写的,而不是为了生产环境。重点是尽可能直接地展示信息流。出于这个原因,笔记本避免了会分散体验的沉重控制结构或错误处理。这些精炼可以在以后构建生产系统时添加。此外,要在本地运行该笔记本,你需要安装 spaCy、Matplotlib 和 Graphviz,并使用命令
python -m spacy download en_core_web_sm下载 spaCy 的英文模型。
让我们开始用 Python 构建一个语义蓝图的可视化工具,它将生成我们的 stemma,这本质上是一个带有语义节点和边缘可视化的图。我们将逐块拆解脚本,解释每个部分的作用。
首先,导入必要的工具。我们的可视化器依赖 matplotlib 进行绘图:
-
matplotlib.pyplot as plt:主绘图接口,使用惯用的别名plt导入以便使用。 -
matplotlib.patches.FancyArrowPatch:用于绘制整洁的定向箭头的工具,将动词与其角色连接。
有了这些工具,我们现在可以定义 SRL 可视化函数了。
主函数:visualize_srl
我们程序的核心是主函数visual_srl()。这是主要的交互点,由图 1.2中的步骤 1 所示。作为用户,你将句子的核心组件作为参数提供,函数负责处理其余工作,组织数据并将其发送给绘图辅助函数。
参数直接对应于之前定义的语义角色。为了保持接口的灵活性,函数还接受**kwargs,这允许你传递任意数量的可选修饰符(例如时间或位置细节),而不会使函数签名复杂化。
visualize_srl()的目的是将这些角色汇成一个字典(srl1_roles),然后将谓语(动词)连同交给内部的_plot_stemma()进行可视化:
def visualize_srl(verb, agent, patient, recipient=None, **kwargs):
"""
创建一个语义蓝图并将其可视化为 stemma。
这是主要面向用户的主函数。
"""
srl_roles = {
"Agent (ARG0)": agent,
"Patient (ARG1)": patient,
}
if recipient:
srl_roles["Recipient (ARG2)"] = recipient
# 添加其他修饰符
for key, value in kwargs.items():
# 格式化 key 为 ARGM-
role_name = f"{key.capitalize()} (ARGM-)"
srl_roles[role_name] = value
_plot_stemma(verb, srl_roles)
注意
该函数创建一个语义蓝图并将其可视化为 stemma,这是主要面向用户的主函数。
统计角色的数量,然后在水平线上创建一组均匀间隔的x_positions列表:
srl_items = list(srl_roles.items())
num_roles = len(srl_items)
x_positions = [10 * (i + 1) / (num_roles + 1)
for i in range(num_roles):
y_position = 4.5
让我们通过绘制角色、箭头和标签来为我们的主干图添加连接。
我们将遍历srl_roles字典中的每个角色。在循环的每次迭代中,我们执行了三个操作:
-
绘制角色节点:我们再次使用
ax.text()在计算出的位置为当前角色绘制框框。 -
绘制连接箭头:我们创建一个连接动词位置的
FancyArrowPatch,并将其添加到我们的图表中。 -
绘制箭头标签:我们计算箭头的中点,并在该处放置角色名称(例如,
Agent (ARG0)),以便清晰显示关系。
随后代码将管理位置:
for i, (role, text) in enumerate(srl_items):
child_pos = (x_positions[i], y_position)
ax.text(child_pos[0], child_pos[1], text,
ha="center", va="center",
bbox=role_style, fontsize=10, wrap=True)
arrow = FancyArrowPatch(
verb_pos,
child_pos,
arrowstyle='->',
mutation_scale=20,
shrinkA=15,
shrinkB=15,
color='gray'
)
ax.add_patch(arrow)
label_pos = (
(verb_pos[0] + child_pos[0]) / 2,
(verb_pos[1] + child_pos[1]) / 2 + 0.5
)
text.text(label_pos[0], label_pos[1],
ha="center", va="center",
fontsize=0, color='black', bbox="square, pad=0.1",
fc="white", ec=None)
最后,我们进行可视化处理并使用plt.show()显示主干提取器:
fig.subtitle("The Semantic Blueprint (Stemma Visualization)",
fontsize=16)
plt.show()
到此时,我们的主函数已经完全定义。我们现在可以运行示例来查看主干提取器的运行情况。
运行 SRL 示例
通过带有不同的参数调用visualize_srl函数,我们可以瞬间将线性句子结构化的语义蓝图。以下示例将加强你对核心语义角色的理解,并展示我们构建的工具的灵活性。
示例 1:商业推演
让我们从本节中一直在解构的句子开始:
Sarah 在早晨向董事会推介了新项目。
对应的 SRL 定义如下:
print("Example 1: A complete action with multiple roles")
visualize_srl(
verb="pitch",
agent="Sarah",
patient="the new project",
recipient="to the board",
temporal="in the morning"
)
在这个示例中,我们为所有核心角色和一个修饰语提供了正确的值:
-
谓语 (Predicate):中心动作是
pitch(推介)。 -
施者 (Agent (ARG0)):推介的人是 Sarah。
-
受者 (Patient (ARG1)):被推介的是新项目。
-
接收者 (Recipient (ARG2)):接收推介的实体是董事会。
-
时间 (Temporal (ARGM-TMP)):动作发生在早上。
运行这段代码会产生图 1.3 所示的主干,其中pitch为根节点,四个子节点代表各个角色:
语义蓝图(主干可视化)
图 1.3:一个根节点四个子节点
让我们继续另一个示例。
示例 2:技术更新
现在,让我们建模另一种类型的句子,这次包含一个位置。考虑这个句子:
后端团队解决了支付网关中的关键漏洞。
SRL 定义如下:
print("Example 2: An action with a location")
visualize_srl(
verb="resolved",
agent="The backend team",
patient="the critical bug",
location="in the payment gateway"
)
以下是组件映射到语义角色的方式:
-
谓语 (Predicate):动作是
resolved(解决)。 -
施者 (Agent (ARG0)):执行解决的实体是后端团队。
-
受者 (Patient (ARG1)):被解决的是关键漏洞。
-
地点 (Location (ARGM-LOC)):漏洞解决位置的上下文是在支付网关。
可视化器生成了图 1.4 所示的主干,resolved位于顶部,连接着它的三个参与者。这清晰地结构化了技术更新。
语义蓝图(主干可视化)
图 1.4:一个根节点三个子节点
任务图结构
-
前置任务:无(这是起始数据)
-
后继任务:g2(提取核心内容)
2. 提取核心内容 (g2)
-
目的:清洗数据;将信号与噪声分离。
-
前置任务:transcript(输入原始转录)。
-
后继任务:这是主要的分叉点。它的输出(
substantive_content)是三个不同并行步骤的直接前置任务:-
g3(识别新进展)
-
g4(分析隐性动态)
-
g5(生成新解决方案)
-
3. 识别新进展 (g3)
-
目的:通过对比旧版总结,寻找新信息
-
前置任务:
substantive_content(来自 g2)和prev_summary(上一次总结) -
后继任务:g6(创建结构化总结)
4. 分析隐性动态 (g4)
-
目的:分析情感和社交潜台词
-
前置任务:
substantive_content(来自第 2 步) -
后继任务:无(通往最终输出
implicit_threads)
5. 生成新解决方案 (g5)
-
目的:将事实合成新的、创造性的想法
-
前置任务:
substantive_content(来自第 2 步) -
后继任务:无(通往最终输出)
6. 创建结构化总结 (g6)
-
目的:将进展格式化为表格。
-
前置任务:g3(识别新进展)。
-
后继任务:g7(草拟后续行动)。
7. 草拟后续行动 (g7)
- 目的:将结构化总结转换为行动项。
工作流指南:从会议转录到跟进邮件
背景与流程概览
-
前置任务:
g6(创建结构化总结)。 -
后继任务:无(通往最终输出
follow_up_email)。
现在让我们将此流程图计划转换为实际代码并分步运行:
- 打开目录下的
Use_Case.ipynb。我们将首先安装必要的OpenAI库。
# Cell 1: 安装
pip install openai
- 此工作流使用 Google Colab Secrets 来存储 OpenAI API 密钥。加载密钥,设置环境变量并初始化客户端:
# Cell 2: 导入和 API 密钥设置
# 我们将使用 OpenAI Library 与 LLM 交互,并使用 Google Colab 的
# 密钥管理器安全地访问您的 API 密钥。
import os
from openai import OpenAI
from google.colab import userdata
# 从 Colab secrets 加载 API 密钥,设置环境变量,然后初始化客户端
try:
api_key = userdata.get("API_KEY")
if not api_key:
raise userdata.SecretNotFoundError("未找到 API_KEY.")
# 为下游工具/库设置环境变量
os.environ["OPENAI_API_KEY"] = api_key
# 创建客户端(将读取 OpenAI_API_KEY)
client = OpenAI()
print("OpenAI API key 加载且环境变量设置成功。")
except userdata.SecretNotFoundError:
print('未找到 Secret "API_KEY"')
print('请将您的 OpenAI API密钥添加到 Colab Secrets Manager。')
except Exception as e:
print(f"加载 API key时发生错误:{e}")
3. 输入数据准备
将会议转录作为多行字符串添加。这将是后续链式步骤的输入:
# Cell 3: 完整会议转录
meeting_transcript = """
Tom: 大家早上好。咖啡开始起效了。
Sarah: 早上好,Tom。好的,让我们开始吧。凤凰项目时间线。Tom,你说后端组件进展顺利,对吗?
Tom: 基本上如此。我们在支付网关集成上遇到了一个小障碍。它……比文档建议的要复杂。我们可能还需要三天时间。
Maria: 三天?Tom,那会让最终测试紧贴发布截止日期。我们没有缓冲余地。
Sarah: 我同意 Maria 的观点。Tom,还有什么替代方案吗?
Tom: 我想我可以在周末加班来赶进度。虽然我并不想这样做,但我能看到我们面临的困境。
Sarah: 感谢,Tom。我们暂定达成共识。Maria,前端情况如何?
Maria: 我们没问题。事实上,我们还领先了一点。我们有一些额外的带宽。
Sarah: 太棒了。好的,最后一件事。营销团队想在发布当天进行一次大规模的社交媒体推广。怎么想?
Tom: 看起来很常规。
Maria: 我认为这是个错误。如果出现任何初始漏洞,一天的巨大推广会淹没我们的服务器。我们应该进行软发布,第一周仅限邀请制,然后再进行大规模推广。这样更可控。
Sarah: Maria,你说得很好。这是一种更稳的策略。我们就这么办吧。好的,非常好的会议。我会发送一份总结。
Tom: 听起来不错。现在,多喝点咖啡。
"""
你已经准备好开始链式操作了。第一个动作将从 meeting_transcript 中分离信号与噪声:提取决策、更新和问题;忽略问候和闲聊。让我们开始吧!
第 1 层:确定范围(“什么”)
我们将首先定义分析范围。如所建立,每个步骤的输出都将馈给网络,创建一个上下文链。
- 准确告诉模型应该忽略什么。
# Cell 4: g2 - 提取实质性内容
prompt_g2 = """
分析以下转录。提取决策、更新和问题。
忽略问候、闲聊和无关的闲谈。
转录内容:
---
{meeting_transcript}
"""
- 现在我们激活 OpenAI 来提取实质性内容:
client = OpenAI()
try:
response_g2 = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt_g2}]
)
substantive_content = response_g2.choices[0].message.content
print("--- 实质性内容 ---")
print(substantive_content)
except Exception as e:
print(f"发生错误:{e}")
| 核心内容 | 详情 |
| :--- | :--- |
| 凤凰项目时间线 | 后端集成比预期复杂;需要额外的三天。 |
| 对进度的影响 | 额外的三天使最终测试紧贴发布截止日期,并减少了缓冲。 |
| 缓解决策 | 暂定同意 Tom 在周末加班赶进度。 |
| 前端状态 | 领先进度,有额外的带宽。 |
| 营销/发布决策 | 从发布当天的大规模推广改为第一周仅限邀请制的发布,然后进行大规模推广。 |
- 下一步,我们通过检索增强生成 (RAG) 将新会议与上一次总结进行对比。
# Cell S: g3 - 识别新的
previous_summary = "在上次会议中,我们确定了凤凰项目的目标并将任务分配给了 Tom 和 Maria。"
prompt_g3 = f"""
背景:上次会议的总结是:"previous_summary}"
任务:分析以下新会议的实质性内容。
识别并总结自上次会议以来的新进展、问题或决策。
新会议内容:
---
{substantive_content}
---
"""
我们现在再次让 OpenAI 执行 RAG 任务:
try:
response_g3 = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt_g3}]
)
new_developments = response_g3.choices[0].message.content
print(f"-- 自上次会议的新进展 --")
print(f"{new_developments}")
except Exception as e:
print(f"发生错误:{e}")
自上次会议的新进展
-
后端方面:支付网关集成比预期复杂;需要额外的三天。
-
对进度的影响:额外的三天使最终测试紧贴发布截止日期,并减少了缓冲。
-
缓解决策:暂定同意 Tom 在周末加班赶进度。
-
前端状态:领先进度,有额外的带宽。
-
营销/发布决策:从发布当天的大规模推广改为第一周仅限邀请制的发布,然后进行大规模推广。
我们已经揭示了范围;换句话说,即转录的“什么”。让我们深入“如何”。
第 2 层:进行调查(“如何”)
现在我们从识别事实转向生成见解,这是语义上下文解释之旅的核心。这里是提示 AI 读言外之意的地方。
- 除了显式事实外,每场会议都带有潜台词:犹豫、紧张和情绪。这一步要求 AI 分析底层的动态:
# Cell 6: g4 - 揭示隐性线索
prompt_g4 = f"""
任务:分析以下会议内容中隐含的社交动态和未表述的情感。请超越字面意思。
- 是否有人在同意某事后表现出犹豫或不情愿?
- 是否存在任何潜在的分歧或紧张关系?
- 整体氛围是怎样的?
会议内容:
---
{substantive_content}
---
"""
- 我们运行提示词来探索会议中字面意思之外的隐含动态:
try:
response_g4 = client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": prompt_g4}]
)
implicit_threads = response_g4.choices[0].message
print("--- IMPLICIT THREADS AND DYNAMICS ---")
print(implicit_threads)
except Exception as e:
print(f"发生了错误: {e}")
这一阶段的输出突显了一些新东西:大语言模型(LLMs)不仅仅是提供事实或总结事件。它们还在挖掘话外之音:
--- IMPLICIT THREADS AND DYNAMICS ---
以下是似乎浮现在水面下的内容:
尽管同意但仍犹豫/不情愿
- Tom 对周末工作的“犹豫”同意读起来像是不情愿。这表明他感到有压力才自愿,而不是出于真实的意愿。
- 营销部门可能带着某些不情愿同意了软发布;从第一天的大规模投入改为仅限邀请制,意味着对工程风险的妥协。
潜在的分歧或紧张关系
- 速度即质量:工程部门想要稳定性和缓冲;而营销部门最初的目标是追求影响力。软发布是一种折中方案,但不同的风险偏好依然存在。
- 工作量公平性:后端落后于前端,而前端有“额外带宽”。让 Tom 在周末加班(而不是重新分配任务)暗示了部门孤岛或一种个人英雄主义的准则,这可能会产生无声的怨恨。
- 测试挤压:紧赶截止日期意味着质量保证(QA)将面临压力,如果漏洞漏掉或发生后期更改,可能会产生摩擦。
- 估算信心:支付网关“比预想的复杂”可能微妙地挑战了早期的估算,引发了对规划或供应商集成假设的无声怀疑。
整体氛围
- 冷静、务实且略显紧张。小组以解决方案为导向并具有协作精神,有一种紧迫感和压力,对计划的宽慰感被对工作量、风险和缓冲减少的担忧所抵消。
这就是上下文链(context chaining)从“记录发生了什么”转向“解释为什么重要”的地方。结果感觉不像是原始转录,而更像是分析师的评论,为我们提供了关于团队动态的未曾说明的洞察。
3. 生成新解决方案
接下来,我们提示 AI 进行创造性思考,通过综合会议中的不同想法来解决问题,展示其思考过程:
# 单元格 7: g5 - 生成新解决方案
prompt_g5 = f"""
背景:在会议中,Maria 建议进行“软发布”以避免服务器崩溃,并提到她的团队有“额外带宽”。
Tom 面临后端延迟 3 天。
任务:提出一个新颖且可行的想法,利用 Maria 团队的额外带宽来帮助缓解 Tom 的3天延迟。将这两条独立的信息合并为一个单一解决方案。
"""
try:
response_g5 = client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": "prompt_g5"}]
)
novel_solution = response_g5.choices[0].message.content
print("-- AI 提出的新颖解决方案 ---")
print(novel_solution)
except Exception as e:
print(f"发生了错误: {e}")
该提示词挑战 AI 考虑到 Maria 的额外前端带宽和 Tom 的后端三天延迟,并将它们织入一个新颖的解决方案。这就是上下文展示其真实实力的地方:我们不是要求回忆或可能的解释,而是推动模型生成一些转录稿中不存在的新内容:
-- AI 提出的新颖解决方案 --
主意:在临时的“边缘桥接”(Edge Bridge)后进行软发布,由 Maria 团队构建,用于在 Tom 后端准备好之前缓冲读写操作。
Maria 团队要做的工作(利用他们的额外带宽)
- 建立一个与真实 API 合约匹配的服务器/API façade(例如 API Gateway/Lambda + SQS/DynamoDB 或 Cloudflare Workers + Durable Objects)。
- 读取:从之前的缓存或静态快照提供服务(过期更新)。通过测试环境的轻量级数据导出或执行端点每小时更新快照。
- 写入:将请求捕获在带持久键的持久队列中;向客户端立即返回“已排队”并显示“同步中”界面。当 Tom 的后端上线后,重放工作程序将清空队列并应用更改。
- 添加功能标记/流量控制(例如 LaunchDarkly)以限制软发布并节流请求以避免服务器压力。
这如何缓解 3 天延迟
- 产品可以针对小部分群体进行软发布,而无需等待后端;用户获得读取访问权限和缓冲写入。
- 当 Tom 的后端准备就绪时,将路由切换到真实后端并清空队列以对齐数据。
行动计划和时间表
- 第 0 天(今天):识别软发布的最小关键端点。(*注:此处原文存在大量重复文本,已按要求去重*)
最终行动:生成跟进邮件
上下文链(context chaining)的最后一步是完成从洞察到行动的闭环:将结构化分析转换为专业的跟进邮件:
# Cell 9: g7 - 起草跟进行动
prompt_g7 = f"""
Task: 基于下方的摘要表,为团队(Sarah, Tom, Maria)起草一封礼貌且专业的
跟进邮件。
邮件应清晰地列出所做的决策以及每个人的行动事项。
Summary Table:
---
{final_summary_table}
---
"""
大语言模型(LLM)将再次承担繁重工作并完成任务:
try:
response_g7 = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "prompt_g7"}]
)
follow_up_email = response_g7.choices[0].message.content
print("--- DRAFT FOLLOW-UP EMAIL ---")
print(follow_up_email)
except Exception as e:
print(f"An error occurred: {e}")
最终行动是基于对指标深度分析的跟进邮件。
跟进:今日同步会议的决策和后续步骤
Hi Sarah, Tom, and Maria,
感谢之前的高效讨论。以下是我们决策的简要回顾以及每个人的行动事项。
决策
-
后端支付网关集成比预想的复杂,需要额外的三时间。
-
这将进度推后了三天,并压缩了最终测试窗口,减少了我们的发布前缓冲时间。
-
缓解措施:Tom 将在周末工作以帮助我们赶上进度。
-
前端进度领先,并有额外的带宽可以支持。
-
上市/营销计划将改为为期一周的仅邀请制软发布,随后进行更大规模的首日推广。
行动事项
Tom
-
确认周末是否有空,并分享一份简要计划(关键里程碑、依赖关系和任何风险)。
-
继续网关集成,并与前端和 QA(质量保证)协调早期集成测试。
-
提供简短的每日进度更新,并立即标记阻塞问题。
Sarah
-
更新项目时间表,反映三天的调整和压缩的 QA 窗口。
-
与 QA 协调适用于缩短周期的基于风险的测试计划。
-
与营销/产品就仅邀请制软发布的范围、成功指标和沟通达成一致;并将计划分发给团队。
Maria
-
重新分配前端带宽以支持后端集成(支付 UI 钩子、错误处理、埋点)。
-
需要时与 Tom 合作进行 mock/stub,以消除早期集成和 QA 的障碍。
-
确保前端准备好进行软发布(功能标记/开关、追踪),并共享任何缺失。
请回复以确认您的行动事项,并说明您需要的任何限制或支持。我很乐意在我们处理此问题期间安排简短的每日检查;如果您有偏好的时间,请提议。
感谢大家,感谢快速的协调。
祝好,
[您的姓名]
这个最终产物展示了上下文链的全部潜力。这封邮件读起来像是由一位勤奋的项目经理编写的。工作流产出的不是编辑抽象的洞察或松散的点点列表,而是专业的沟通。AI 不仅总结了会议,还将其转换为能够实现以下功能的格式:
-
清晰捕获决策,使对达成的内容不再产生歧义
-
分配所有权,确保每项任务都落实到责任人
-
设定预期,如时间表、后续步骤和问责制
-
减少跟进阻力,因为草案已经足够完善可以直接发送,节省了人类的时间和精力
这就是 LLM 停止做“记笔记员”转变为“创意伙伴”的时刻,正如我们在本章开头提到的那样。在这种情况下,我们不仅获得了一个摘要。我们希望与 AI 作为伙伴一起进行思考。人类仍然处于过程的核心,并为会议、邮件处理、报告以及你可以想象的任何场景创建上下文链的模板。使用得当,上下文链可以提升公司和客户的运营方式。
现在让我们退后一步,总结一下我们取得的成就,并进入上下文工程的下一个探索。
总结
本章介绍了上下文工程这项新兴技能,它可以将 LLM 变为可靠的、以目标为导向的系统。不再依赖非结构化的提示,而是展示了控制如何来自对信息环境的工程化,最终以语义蓝图(semantic blueprint)作为最精确的指导形式。
我们通过五个级别的演进追溯了这一转变:从产生通用输出的零上下文提示,到线性的、以目标为导向的基于角色的上下文,证明了结构化输入能驱动更好的结果。为了形式化这种方法,我们引入了 SRL(语义谓语标记),这是将句子分解为谓语、代理者、意图和修饰符的方法,并配有一个 Python 可视化工具将这些角色渲染为架构图。
最后,我们在意义分析用例中应用了这些技能,上下文链将原始转录转变为可操作的结果。通过逐步进行,该过程减少了噪声,突出了新进展,揭示了隐性动态,并产生了结构化的摘要和跟进行动。
SRL 和上下文链分别提供了理论框架和实践工作流,让我们超越简单的提示(prompting)。我们现在准备好在下一章中构建智能体上下文了。
问题
-
上下文工程的主要目标是将 LLM 变为可靠的、以目标为导向的系统。
-
“2级:线性上下文”提供了足够的信息来控制 LLM 吗?
-
第 5 级的“语义蓝图”是最有效的指导形式吗?
-
语义谓语标记(SRL)的主要功能是什么?
-
在句子
Sarah pitched the project中,Sarah被识别为代理者(agent)吗? -
参数修饰符(Argument Modifiers)是 SRL 的核心部分吗?
-
章节用例依赖于单一复杂的提示吗?
-
“上下文链”技术是否定义为使用输出?
-
在工作流中,隐性动态(Implicit Dynamics)是否用于提取事实?
-
本章分析结束于创建一个可操作的产物吗?
参考文献
-
Tesnière, L. (1959). Éléments de syntaxurale. Klinck.
-
Fillmore, Charles J. 1968. “The Case for Case.” In Universals in Linguistic Theory, edited by Ramon Bach and Robert T. Harms, 1–88. New York: Holt, Rinehart and Winston.
-
Palmer, Martha, Danielle Gideon and Paul Ginsbury. 2005. “The Proposition Bank: An Annotated Corpus of Semantic Roles.” Computational Linguistics 31 (1): 71–106.
延伸阅读
-
Brown, T, Mann, B., Ryder, N, Subbiah, S., Kaplan, J. D., Dhariwal, P., Neelakantan, A, et al. 2020. “Language Models Are Few-Shot Learners.” Advances in Neural Information Processing Systems 33: 1877–1901.
-
Wei, J., Wang, X., Schuurmans, D., Bosma, M., Chi, E. H., Le, Q. V., and Zhou, D. 2022. “Chain-of-Thought Prompting Elicits Reasoning in Large Language Models.” Advances in Neural Information Processing Systems 35: 24824–24837.
获取本书的 PDF 版本和额外内容
扫描二维码(或访问 packtub.com/unlock)。通过名称搜索此书,确认版本,然后按照页面步骤操作。
注意:请保留发票。直接从 Packt 购买的不需要发票。
使用 MCP 构建多智能体系统
在上一章中,我们建立了上下文工程的基础技能:将模糊的提示词转换为结构化的语义蓝图。现在,我们将在更大规模的范围内应用这项技能,构建一个由专门的 AI 代理组成的系统,它们通过上下文进行通信并协作解决复杂问题。单一的大语言模型(LLM)是一个出色的通用全才,但它并非专家。对于任何复杂的多步骤任务,使用单个 LLM 效率低下且往往会失败。因此,我们将构建一个多智能体系统(MAS),在这个系统中,我们工程化的上下文将超出单个消息的内容,用于定义系统本身的设计。这包括定义需要哪些代理、它们的特定角色和能力,以及它们在不丢失信息的情况下通信的方法。这才是上下文工程的真正范围。
此外,为了使我们的系统更加可靠,我们将实现模型上下文协议(MCP),这是一种共享语言,确保我们工程化的上下文以完美的保真度在代理之间传递。在实操部分,我们将从零开始构建一个 MAS-MCP 系统。你将学习如何设计管理工作流的协调器(Orchestrator)以及执行任务的专家代理(研究员 Researcher 和编写者 Writer)。你还将看到如何将错误处理、消息验证和自动化质量控制直接构建到工作流中,确保系统能够从故障中恢复并保持事实准确性。到最后,你将拥有一个功能完整的系统,它可以接收高级目标并自主管理复杂的多步骤过程。简而言之,我们将涵盖以下主题:
-
构建上下文驱动的 MAS 工作流
-
使用 MCP 构建一个管理上下文的 MAS
-
错误处理与验证
-
AI 架构的演进
-
构建智能体系统的工具
让我们从构建系统开始吧。
使用 MCP 架构 MAS 工作流
在编写第一行代码之前,我们需要一个清晰的架构计划。本节将阐述该蓝图。我们将总体目标解构为核心组件,定义系统每个部分的作用,并映射出让代理团队运转起来的通信流。
首先,让我们定义该设计核心的两个概念:MAS 和 MCP:
-
MAS:我们将设计一个可以运行多个独立代理的系统,每个代理专门负责不同的任务,如研究、写作或数据分析。通过为每个代理提供清晰的上下文,我们确保它能够在其特定的职责上表现出色。
-
MCP:为了让代理能够协作,它们需要一种共享语言。MCP 为我们提供了代理之间如何传递任务和信息的规则。它提供了一个框架,确保每条消息都是结构化的、可靠的且被完美理解。
我们的 MAS 由三个不同的组件组成。每个组件都扮演着特定的角色,它们共同构成了一个能够将用户的高级目标转化为成品的工作流:
多智能体系统
流程图说明了系统的完整工作流。让我们分析这个认知流水线每个组件的作用:
-
协调器(Orchestrator,项目经理):协调器是操作的大脑。它本身不执行专门的任务,而是管理整个工作流。它接收用户的高级目标,将其分解为逻辑步骤,并将每个步骤委托给正确的代理。它还负责接收一个代理的结果并将其作为上下文传递给下一个代理。换句话说,它在系统级对我们的 MAS 应用了上下文链。
-
研究员代理(Researcher agent,信息专家):这是我们的第一个专门代理。它的目的是获取特定主题,寻找相关信息,并将这些信息综合成结构化的摘要。在我们的项目中,它将接收来自协调器的研究任务,并以清晰的符号列表形式返回结果。
-
编写者代理(Writer agent,内容创造者):这是我们的第二个专门代理。它的长处在于通信和创造性表达。它接收研究员提供的结构化摘要,并将其转化为精炼的、人类可读的内容,并仔细注意语气、风格和叙述。
信息并非仅仅以原始文本的形式流动。代理之间的每一次交互都被封装成结构化的 MCP 消息。这确保了任务和结果始终带有完整的上下文,以一致、可预测且可靠的格式进行传递。MCP 是将一组独立代理连接成一个集成系统的结缔组织。
蓝图准备就绪后,我们已经准备好开始使用 MCP 构建我们的 MAS 了。
注意
在本书中,我们探索了将 MCP 原则应用于代理与代理通信的尖端定义。虽然 MCP 最初是为代理与工具的交互设计的,但最近的探索(例如微软的《你可以在 MCP 上构建代理与代理之间的通信吗?》)展示了 MCP 不断进化的能力如何支持新兴的智能体协调模式。
文章链接:
https://developer.microsoft.com/blog/can-you-build-agent2agent-communication-on-mcp
使用 MCP 构建 MAS
现在我们已经有了蓝图,是时候开始编写代码了。在这一节中,我们将逐步实现 MAS 和 MCP。我们将从系统的核心功能开始,然后在后的“错误处理与验证”章节中返回添加错误处理和验证。
打开本章存储库中的Open_MCP.ipynb进行跟随。初始的 OpenAI 安装与第1章中的SRL.ipynb相同。
我们将通过以下步骤完成 Colab 单元:首先定义我们的协议,构建每个专家代理,构建协调器来管理它们,最后运行系统查看代理团队的操作。让我们先初始化 OpenAI 客户端。
初始化客户端
我们首先初始化 OpenAI 客户端,它将作为我们与 LLM 通信的网关。我们导入了json库以整洁的格式显示我们的消息:
#@title 1. 初始化客户端
# 我们需要 openai 库来与 LLM 通信。
# 注意:此笔记本假设你已经在 Colab 环境中运行了设置单元,
# 将 Secrets 中的密钥加载到环境变量中,正如你所指定的。
import json
from openai import OpenAI
# 客户端将从环境中读取 OpenAI_API_KEY。
client = OpenAI()
print("OpenAI client initialized.")
输出显示确认消息:
OpenAI client initialized.
定义协议
正如之前提到的,我们的代理需要一种共享语言来协作。这种语言由 MCP 定义。在本节中,我们将实现一个简化版本来演示该过程。虽然我们的 Python 方法对于学习非常有效,但值得了解规范系统间通信的官方 MCP 规则。
消息格式
MCP 消息的结构有严格定义以确保一致性:
-
所有消息均遵循 JSON-RPC 2.0 格式作为清晰的 JSON 对象
-
消息必须使用 UTF-8 编码以实现通用性
-
每条消息必须单行显示,不包含换行符,以便快速可靠地解析
传输层
一旦定义了消息,MCP 就定义了传输层。传输层定义了代理之间传递消息的方式。两种主要方法如下:
-
STDIO(标准输入/输出):对于运行在同一机器上的代理(例如在我们的 Colab 笔记本中),它们可以通过标准输入输出直接进行通信。这是最简单直接的方法。
-
HTTP:对于运行在不同服务器上的代理,消息通过标准的 HTTP 请求在互联网上传发送。
最后,我们需要一个协议管理框架。
协议管理
MCP 还包含了关于兼容性和安全性的规则:
-
版本控制:使用 HTTP 时,需要版本标头以确保客户端和服务器使用同一套规则。
-
安全性:有用于验证连接的规则,以防止常见的网络攻击,并确保你正在与预期的服务器进行通信。
对于本书中的实操项目,我们将通过练习结构化通信来关注 MCP 的精神。一个简单的 Python 字典足以说明这一理念。它可以作为正式 JSON-RPC 对象的替身,让我们能够清晰地看到消息是如何构建和传递的,而不会迷失在协议开销中。
准备工作,我们现在可以创建一个名为create_mcp_message的辅助函数。该函数是我们系统中传输消息的模板。通过每次使用相同的结构,我们确保信息永远不会丢失或被误解。以下是代码:
## 2. 定义协议:MCP 标准
# 在构建智能体之前,我们必须定义它们将使用的语言。
# MCP 提供了一种简单、结构化的上下文传递方式。在本示例中,
# 我们的 MCP 消息将是一个带有关键字段的 Python 字典。
def create_mcp_message(sender, content, metadata=None):
"""
创建一个标准的 MCP 消息。
"""
return {
"protocol_version": "1.0",
"sender": sender,
"content": content,
"metadata": metadata or {}
}
print("--- 示例 MCP (我们的简化版) ---")
example_mcp = create_mcp_message(
sender="Orchestrator",
content="研究地中海饮食的好处。",
metadata={"task_id": "T-123", "priority": "high"}
)
print(json.dumps(example_mcp, indent=2))
输出显示了我们的 JSON 格式的结构化 MCP 消息示例:
--- Example MCP Message (Our Simplified Version) ---
{
"protocol_version": "1.0",
"sender": "Orchestrator",
"content": "Research the benefits of the Mediterranean diet.",
"metadata": {
"task_id": "T-123",
"priority": "high"
}
}
create_mcp_message 函数接收三个输入:发送者、内容(任务或信息)以及任何可选元数据。它始终返回一个相同结构的标准 Python 字典。这种一致性是我们系统可靠性的基础。消息在智能体之间流动,没有丢失、误读或误解的风险。
定义好协议后,我们准备好构建专家智能体了。
构建智能体
本节包含了将我们在图 2.1 工作流图中的智能体变为栩生运行的代码。我们将每个智能体定义为一个 Python 函数。每个智能体函数都将接受一个结构化的 MCP 消息作为输入,并返回另一个 MCP 消息作为输出。智能体的特定角色由其系统提示词(system prompt)塑造,它告诉 LLM 如何行为。为了保持通信的一致,我们还将创建一个名为 call_llm 的单一辅助函数,用于管理与 OpenAI API 的所有交互。
多智能体 AI 系统流程图
研究员智能体 (Researcher Agent)
该图说明了一个使用两个 AI 智能体创建博客文章的工作流。信息从初始想法流到完成的内容经过了一系列步骤:
-
流程始于一个研究主题,这是启动工作流的提示词。包含主题的消息被发送给我们的第一个智能体——研究员智能体(Researcher agent),它从模拟的内部数据库收集相关的事实和信息。
-
一旦研究员获得了原始数据,它将使用 LLM API(一种外部语言模型服务)来处理信息并创建关键发现的摘要。该摘要是过程上一个和下一个阶段之间的桥梁。
-
摘要被传递给第二个智能体——作者智能体(Writer agent),它的角色是将研究点转化为完成的文章。作者智能体也会与 LLM API 通信,但是为了执行复杂且具有创造性的任务。
-
系统的最终结果是一个博客文章,清晰地展示了专业智能体如何协同工作,将高级目标转化为完整的产物。
我们现在准备好实现相互通信的智能体了。
创建辅助函数
在构建智能体本身之前,我们需要一种可靠且一致的方式让它们与 LLM 通信。我们的辅助函数 call_llm 处理所有到 OpenAI 的 API 调用。我们的系统使用提示词和用户内容作为输入,并返回模型的响应:
def call_llm(system_prompt, user_content):
"""使用新客户端语法调用 OpenAI API 的辅助函数"""
try:
# 使用更新后的 client.chat.completions.create 方法
response = client.chat.completions.create(
model="gpt-3",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content}
]
)
return response.choices[0].message.content
该函数有两个输入:
-
system_prompt,一个告诉模型如何行为的字符串。 -
user_content,包含用户查询的字符串。
定义智能体
def researcher_agent(mcp_input):
"""
该智能体接收一个主题并从数据库中查找相关信息。
"""
print("\n[研究员智能体已激活]")
simulated_database = {
"keto diet": "生酮饮食富高脂肪、低碳水和中等蛋白质。",
"mediterranean diet": "地中海饮食富含水果、蔬菜、全谷物、橄榄油和鱼类。研究显示它与心脏病风险较低。",
"intermittent fasting": "间歇性禁食是一种在进食和禁食之间循环的饮食模式。",
}
适合健康与养博客的鼓励性语气。提示词还规定了长度(约 150 字)并要求一个引入人的标题:
system_prompt = "你是一位健康与养博客的资深内容写作者。你的语气平易近人、信息量丰富且具有鼓励性。你的任务是根据以下研究点撰写一篇短小、吸引人的博客文章(约 150 字),并带上引入人的标题。"
代理调用 call_llm,传入系统提示词和研究摘要。返回的文本就是我们的博客文章。我们还打印了一条消息以确认稿已创建:
blog_post = call_llm(system_prompt, research_summary)
print("Blog post drafted.")
最后,使用 create_mcp_message 将博客文章封装进新的 MCP 消息。这确保了输出遵循与其他所有消息相同的结构化格式,并包含内容元数据;在此情况下,我们统计了以下字数:
return create_mcp_message(
sender="WriterAgent",
content=blog_post,
metadata={
这完成了 Writer(写作者)。它接收来自 Researcher(研究者)的结构化输入,应用新的系统提示词,并输出格式一致的 MCP 完整博客文章。
当 Researcher 和 Writer 都准备完成后,我们现在定义了 MAS(多代理系统)的核心。每个代理都有独特的角色,它们共同可以完成从原始信息到最终内容的任务。
构建编排器 (Orchestrator)
我们现在有了专门代理,但我们需要一种管理它们的方法。这个角色属于编排器。把它想象成我们 AI 团队的项目经理。它的工作是接收一个高级目标,将其分解为一系列任务,并将这些任务委托给正确的代理。它还管理信息流,将一个代理的输出作为下一个代理的输入。这创建了一个从开始到结束的无缝流程:
编排器
图 2.3:代理的编排器
上图的工作流在初始目标被发送给编排器时开始。编排器充当中心枢纽,首先向Researcher(研究者代理)发送任务。一旦研究完成,该代理将其发现返回给编排器。编排器处理这些信息并向Writer(写作者代理)发送新任务。在 Writer 完成后,它将完成的内容传回编排器。最后,编排器拥有了所有内容来生成最终输出,完成整个过程。
以下代码定义了我们的编排器。这个单一函数管理从开始到结束的多代理工作流。它的角色是调用 Researcher,然后是 Writer,最后将输出组装成一个完整的制品。让我们逐步分析:
def orchestrator(initial_goal):
"""
管理多代理工作流以实现高级目标。
"""
print("=" * 50)
print(f"[Orchestrator] Goal Received: '{initial_goal}'")
print("=" * 50)
我们从开始定义编排器函数。它接受一个参数 initial_goal,代表我们希望系统完成的高级任务。该函数随后打印已接收目标的确认。
第一步是委托研究任务。在这里,我们将"地中海饮食"硬编码为主题,但在实践中,这可以根据用户的目标动态设置:
# 步骤 1:编排器规划并调用 Researcher 代理
print("\n[Orchestrator] Task 1: Research. Delegating to Researcher Agent.")
research_topic = "Mediterranean Diet"
我们将研究主题封装在 MCP 消息中,以保持通信的一致性:
mcp_to_researcher = create_mcp_message(
sender="Orchestrator",
content=research_topic
)
编排器现在使用 MCP 消息调用 Researcher 代理,接收响应并打印返回的摘要:
mcp_from_researcher = researcher_agent(mcp_to_researcher)
print("\n[Orchestrator] Research complete. Received summary:")
print("-" * 20)
print(mcp_from_researcher['content'])
print("-" * 20)
编排器现在在 mcp_from_researcher 中拥有 Researcher 代理的响应,它可以将其传递给 Writer 代理。Writer 代理反过来首先在 mcp_to_writer 中创建 MCP 消息并发送给 Writer 代理,然后显示结果(这将在我们运行系统时出现)。然后它记录写作步骤已完成:
# 步骤 2:编排器调用 Writer 代理
print("\n[Orchestrator] Task 2: Write Content. Delegating to Writer Agent.")
mcp_to_writer = create_mcp_message(
sender="Orchestrator",
content=blog_post,
metadata={
)
mcp_from_writer = writer_agent(mcp_to_writer)
print("\n[Orchestrator] Writing complete.")
最后,编排器获取 Writer 的响应并呈现最终结果:
### 步骤 3:编排器呈现最终结果
final_output = mcp_from_writer['content']
print("\n[Orchestrator] Final Output:")
print(final_output)
至此,编排器函数完成。
运行系统
我们已经准备好了所有组件:Researcher 代理、Writer 代理和编排器。现在让我们运行系统并观察它们的工作。
编排器接收目标,传递给 Researcher,获取摘要,然后传递给 Writer,后者生成博客文章。
## 运行系统
user_goal = "Create a blog post about the benefits of Mediterranean diet."
orchestrator(user_goal)
图 2.4:代理的流程
多代理执行流
| 阶段 | 动作 | 输出 |
| :--- | :--- | :--- |
| 用户输入 | 目标到编排器 | 关于地中海饮食好处的博客 |
| 编排器 | 任务 1:研究 | 委托给 Researcher 代理 |
| Researcher 代理 | 研究完成 | 地中海饮食摘要 |
| 编排器 | 任务 2:编写内容 | 委托给 Writer 代理 |
| Writer 代理 | 写作完成 | 博客文章草稿 |
| 编排器 | 最终输出 | Mediterranean Magic... |
现在让我们看看 MAS 的实际输出。我们将分解控制台日志,显示每个代理如何贡献于最终的博客文章:
- 系统在我们给编排器一个目标时开始。编排器确认该目标并准备将其分解为第一个任务:
=============================================
[Orchestrator] Goal Received: 'Create a blog post about the benefits of Mediterranean diet.'
=============================================
Orchestrator Delegates to the Researcher
- 接下来编排器知道了。它宣布研究任务并激活 Researcher 代理。Researcher 随后工作并告诉我们它创建了一个摘要。
多代理系统(MAS)工作流演示
系统运行示例
[Orchestrator] Task 1: Research. Delegating to Researcher Agent.
[Researcher Agent Activated]
Research summary created for 'Mediterranean Diet'
在 Researcher 完成任务后,编排器确认已收到摘要。随后它显示 Researcher 提取的要点。此输出了将用于博客文章的信息:
[Orchestrator] Research complete. Received summary:
------------------
- 强调蔬菜、全谷物、橄榄油和鱼类食物。
- 与心脏病风险降低、大脑健康改善和长寿增加有关。
- 益处与高摄入单不饱和脂肪(特别是来自橄榄油)和抗氧化剂有关。
------------------
拿到摘要后,编排器进入下一步。它宣布它正将内容撰写任务委托给 Writer 代理。Writer 开始起草博客文章:
[Orchestrator] Task 2: Write Content. Delegating to Writer Agent.
[Writer Agent Activated]
博客文章草稿已拟定
当 Writer Agent 开始起草博客文章时,编排器(Orchestrator)会确认写作任务已完成:
[Orchestrator] Writing complete.
最后,编排器信号整个工作流已完成。随后它呈现了最终的博客文章,这是所有智能体共同努力的成果:
[Orchestrator] Workflow Complete. Final Output:
地中海魔法:增强心脏、大脑和长寿的美味之道
地中海风格的餐盘推崇色彩丰富的果蔬、五谷物、丝滑的橄榄油以及大量的鱼。研究表明,这种饮食模式与心脏病风险降低、大脑更健康以及更长寿有关。秘诀是什么?单不饱和脂肪——尤其是特级橄榄油——有助于改善胆固醇平衡并保持血管弹性,而富含抗氧化剂植物和海鲜则对抗氧化压力和炎症……
这次运行展示了我们多智能体系统(MAS)的生命周期。在这个阶段,系统按预期工作。网络波动、格式错误的消息或意外的 LLM 响应可能会中断流程。为了让工作流更加可靠并更接近生产就绪,我们需要通过错误处理、验证和保护措施来加强它。这正是我们下一步要转的地方。
错误处理与验证
我们目前为止构建的系统是一个功能性原型;当一切按计划进行时,它可以运行。但在现实世界中,事情很少按计划进行。API 可能会失败,消息可能格式错误,智能体可能会返回意外结果。在考虑其健壮性之前,我们必须改进系统以应对这些潜在的故障。
在本节中,我们将把简单的脚本升级为更具韧性的系统。我们将为 API 调用添加错误处理,创建一个验证器以确保 MCP 消息格式正确,并教编排器在智能体失败或产生缺陷输出时如何保持工作流运行。
在这一部分,我们将在 MAS_MCP_control.ipynb 笔记本中工作,其中包含了完整的实现。我们将关注两个核心工程原则:韧性和可靠性:
-
韧性 (Resilience):加固与 LLM 的连接,使临时问题(如 API 超时或速率限制)不会导致系统崩溃
-
可靠性 (Reliability):确保消息完整性,使智能体始终交换可预测、有效的 MCP 消息
让我们从提高韧性开始。我们原始的 call_llm 函数是有意设计的。我们发送单次 API 请求并返回结果。在生产环境中,这种方法太脆弱了。OpenAI API 可能会宕机,或者如果请求因网络不稳定失败,整个工作流可能会崩溃。为了修复,我们将构建一个更健壮的函数,它可以处理重试。
为 LLM 构建健壮组件
加强我们系统的第一步是将原始 call_llm 替换为更具韧性的函数:call_llm_robust。这个新版本获取请求而不是立即放弃,允许系统从临时问题中恢复。
在定义函数之前,我们需要 time 库,它允许我们在重试之间暂停执行。这已经包含在 MAS_MCP_control.ipynb 的初始化部分中:
import time
准备好后,这里是新函数:
# @title 3.Building Robust Components
# Hardening the call_llm Function
def call_llm_robust(system_prompt, user_content, retries=3, delay=5):
"""一个健壮的辅助函数,用于重试调用 OpenAI API."""
for i in range(retries):
try:
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content}
]
)
return response.choices[0].message.content
except Exception as e:
print(f"API call failed on attempt {i+1}/{retries}. Error: {e}")
if i < retries:
print(f"在 {delay} 秒后重试...")
time.sleep(delay)
else:
print("所有重试均失败。")
return None
在这个升级后的函数 call_llm_robust 中,我们将 API 调用封装在循环内的 try/except 块中。如果发生错误,系统将等待几秒后再次尝试,并在尝试之间通过 time.sleep 暂停。这种重试机制使系统对临时网络问题更具韧性。如果所有尝试均失败,函数返回 None,这向编排器信号它必须决定如何处理故障。
在建立了 API 用的韧性后,让我们转向可靠性,引入一个在每个 MCP 消息到达智能体之前对其验证的函数。
验证 MCP 消息
为了让智能体可靠地通信,它们必须信任接收的消息。如果漏掉格式错误的消息,整个工作流可能会失败。为了防止这种情况,我们引入了 MCP 验证器,这是一个简单的护栏,检查每条消息是否符合我们的协议。检查在消息传递给智能体之前进行:
# The MCP Validator ---
def validate_mcp_message(message):
"""一个检查 MCP 消息结构的简单验证器。"""
required_keys = ["protocol_version", "sender", "content", "metadata"]
if not isinstance(message, dict):
print(f"MCP 验证失败:消息不是字典。")
return False
for key in required_keys:
if key not in message:
print(f"MCP 验证失败:缺少键 '{key}'")
return False
print(f"来自 {message['sender']} 的 MCP 验证通过。")
return True
此函数是一个重要的护栏。编排器在传递任何消息之前调用 validate_mcp_message,确保结构完整。它首先检查输入是否为字典,然后验证所有必要的键是否存在。
有了这个验证器,编排器可以信任它分发的消息,智能体也可以专注于自己的任务。结合为 API 调用增加韧性的 call_llm_robust 函数,我们现在有了一个既可靠又容错的系统基础。
现在让我们通过为专家引入控制来添加另一种保护措施。
添加智能体专项控制
我们目前的智能体团队可以工作,但有一个弱点:它完全信任 Writer 的输出。即使有良好的上下文,LLM 也会误解事实或引入幻觉。为了让我们的系统更加可靠,我们需要一个质量控制步骤,在接受 Writer 的工作之前对其检查。
首先,更新智能体。我们在两个智能体内部将旧的 call_llm 函数替换为新的 call_llm_robust 函数:
### title 4.Building the Agents: The Specialists
---
#### Agent 1: The Researcher ---
def researcher_agent(mcp_input):
... (code omitted for brevity)
system_prompt = "你是一名研究分析师。将提供的信息综合为3-4个简洁的要点。"
# 现在使用健壮调用器
summary = call_llm_robust(system_prompt, research_result)
... (code omitted for brevity)
#### Agent 2: The Writer ---
def writer_agent(mcp_input):
... (code omitted for brevity)
system_prompt = "你是一名内容撰稿员。根据以下研究点写一篇简短、吸引人的博客文章(约 150 字),并带有一个引人的标题。"
# 现在使用健壮调用器
blog_post = call_llm_robust(system_prompt, research_summary)
... (code omitted for brevity)
使用 MCP 构建多代理系统
这确保了研究人员(Researcher)和作者(Writer)都能通过重试失败的请求而不是直接报错来应对临时 API 中断。这些代理现在更具韧性,但我们仍然需要一种来验证作者的草稿是否与研究人员的内容一致。下一步是将团队从两个专家扩展到三个。我们引入了验证代理(validator agent)。
验证代理的唯一目的是担任事实检查员。它将作者的草稿与研究人员的摘要进行比较。这种比较确认了事实的一致性。它为我们的系统添加了重要的质量控制层:
# --- Agent 3: The Validator
def validator_agent(mcp_input):
"""该代理根据摘要对草稿进行事实检查."""
print("\n[Validator Agent Activated]")
# 提取两个必要信息
source_summary = mcp_input['content']['summary']
draft_prompt = mcp_input['content']['draft']
system_prompt = """
你是一个严谨的事实检查员。判断"草稿"是否与"源摘要"在事实上一致。
- 如果草稿中的所有主张都得到了源的支持,请仅回复单词"pass"。
- 如果草稿包含源中没有任何信息,请回复"fail"并提供一句话的解释。
"""
validation_context = f"SOURCE SUMMARY:\n{source_summary}\nDRAFT:\n{draft_prompt}"
validation_result = call_llm_robust(system_prompt, validation_context)
print(f"Validation complete: {validation_result}")
return create_mcp_message(
sender="ValidatorAgent",
content=validation_result
)
验证代理有其自己的语义蓝图。它需要两个输入:来自研究人员的源摘要和来自作者的博客草稿。如果草稿与摘要一致,模型返回 pass;如果不一致,则返回 fail 并附带简短解释。
第 2 章
这为编排器(Orchestrator)提供了一个新的保障:它不再需要盲目地信任作者。相反,在接受草稿作为最终版本之前,它可以检查草稿是否与研究结果对齐。
我们现在将添加编排逻辑控制,将验证代理引入工作流。
带有验证循环的最终编排器
原始的简单编排器是一个线性任务管理器。我们用一个更智能的 final_orchestrator 替换了它。这个新版本包含验证修订循环。
工作流不再是顺序的。在 writer_agent 生成草稿后,编排器将其委托给 validator_agent。如果验证失败,编排器会将草稿发回给作者并包含验证代理的反馈。这创建了一个强大的自纠系统,模拟了现实世界的编辑流程。
我们现在将检查笔记本第 5 节中 final_orchestrator 函数的关键变化。
步骤 1 与原始编排器类似。关键的添加是立即调用了 validate_mcp_message。如果研究人员返回了格式错误的消息,或者内容为空(当 call_llm_robust 返回 None 时可能会发生),编排器将立即停止工作流。
@title 5. Final Orchestrator with Validation Loop
def final_orchestrator(initial_goal):
... (省略了初始化代码) ...
# 步骤 1 研究与立即验证
# 步骤 1: Research --
print("\n[Orchestrator] Task 1: Research. Delegating to Researcher Agent.")
# (调用 researcher_agent) ...
mcp_from_researcher = researcher_agent(mcp_to_researcher)
# 新增:立即验证消息结构
if not validate_message(mcp_from_researcher) or not mcp_from_researcher['content']:
print("由于来自研究人员的消息无效或为空,工作流失败。")
return
research_summary = mcp_from_researcher['content']
print("\n[Orchestrator] Research complete.")
使用 MCP 构建多代理系统
随后步骤 2 和 3 被合并为一个迭代循环。如果验证通过,则接受草稿并结束循环。如果验证失败,编排器会将带有反馈的作者草稿发回给作者,要求修订。循环允许有限次数的修订(max_revisions)以防止无限循环:
# 步骤 2 和 3 迭代循环
# 步骤 2 & 3: Iterative Writing and Validation Loop
---
final_output = "Could not produce a validated article."
max_revisions = 2
for i in range(max_revisions):
print(f"\n[Orchestrator] Writing Attempt {i+1}/{max_revisions}")
# 为作者准备上下文
writer_context = research_summary
if i > 0:
# 这是一个修订,基于验证代理的反馈
writer_context += "\nPlease revise the previous draft based on this feedback: (validation_result)"
## 调用作者代理并验证
mcp_to_writer = create_message(sender="Orchestrator", content=writer_context)
mcp_from_writer = writer_agent(mcp_to_writer)
if not validate_mcp_message(mcp_from_writer) or not mcp_from_writer['content']:
print("由于来自作者的消息无效,中止修订循环。")
工作流不再是顺序的。在 writer_agent 生成草稿后,编排器将其委托给 validator_agent。如果验证失败,编排器会将带有反馈的草稿发回给作者并要求修订。这创建了一个强大的自纠系统,模拟了现实世界的编辑流程。
AI 架构的演进
我们构建 AI 的方法正在发生进化。这些模型非常强大且知识渊博,但它们并不是任何单一领域的专家。当任务发生变化时,单一模型很难同时保持专注和准确性。
因此,我们从使用单一通用模型转向创建一个由专业化代理(agents)组成的团队。我们利用上下文工程(context engineering)来定义代理的角色和技能。我们的研究员(Researcher)代理和编写者(Writer)代理就是这种方法的示例。每个代理的设计目标都是为了很好地完成其单项工作,如下所示:
专家团队需要协作来解决大问题。MAS(多智能体系统)提供了管理这些代理的结构。MCP 则帮助代理之间进行可靠的通信。上下文工程是这一过程中的胶水:首先用于改进单个 LLM 的响应,现在则扩展到定义代理本身及其交互。随着 AI 越来越多地部署到现实世界,这些系统将不断扩展和扩展。
构建代理系统的工具
到目前为止,我们从零开始构建了一个 MAS。这种实践方法让我们在 MAS 和 MCP 的核心思想方面拥有了坚实的基础:编排、结构化通信、韧性和验证。
在许多项目中,客户或内部管理层可能要求我们避免外部平台或框架,即使是可下载的开源框架也是如此。某些甚至坚持只使用本地 LLM。然而,在其他情况下,使用框架可能会受到鼓励甚至被要求。那么,上下文工程师的正确路径是什么?
首先,从理解上下文工程开始,就像我们在这里所做一样:从零开始。有了这些知识,你将准备好实现一个完全自定义的解决方案或部署一个优于许多其他工程师的现有框架。你既是架构师又是问题解决者,当即用框架无法满足需求时,寻找让系统重新运行的方法。以下是一些你可以探索的资源:
-
MCP 规范:官方 MCP 规范是必读的。它详细说明了代理如何通信的正式规则。我们构建了一个简化版本。官方网站定义了 JSON RPC 传输层安全标准:
modelcontextprotocol.io。 -
MAS 框架:几个开源框架使得创建 MAS 变得容易得多。它们处理了困难的部分,如编排、通信和代理管理。这些工具使用了与我们刚刚构建相似的概念。学习它们是观察这些想法如何在生产环境中使用的极佳方法。
-
autogen:
autogen是来自微软的一个框架。它让你能够构建多个代理相互对话以解决问题的应用程序。autogen代理非常灵活。它们可以结合 LLM、人类输入和工具来完成工作。你可以在此处的 GitHub 上找到autogen:github.com/microsoft/autogen。 -
CrewAI:
CrewAI专为构建基于角色的 AI 代理团队而设计。它为代理共同协作完成复杂任务提供了结构。CrewAI专注于协作和专业化。它的设计与我们创建的协调器(Orchestrator)模型非常相似。在此查找更多信息:https://www.crewai.com/ -
LangGraph:
LangGraph是 LangChain 工具家族的一部分。它允许你将代理工作流构建为图。这是创建可以迭代自我修正系统的强大方式。LangGraph让你对信息流和决策如何产生进行精细控制。LangGraph文档可以在此处找到:python.langchain.com/docs/langgraph。
至此,我们已经从零开始构建了一个 MAS 和 MCP 用例,以获得对尖端上下文工程系统的实践理解。让我们总结一下我们的旅程。
总结
在本章中,我们超越了单一代理交互。我们构建了一个专业代理团队,每个代理执行特定的特定功能。我们利用了 MCP,它允许我们的代理使用标准蓝图进行通信。然后,我们设计了系统架构。我们创建了一个语义蓝图来定义代理团队的角色和工作流。接下来,我们部分地构建了系统。我们从一个创建标准 MCP 消息的功能开始。然后构建了每个代理,为每个代理提供具有特定提示(prompt)的独特技能。我们构建了研究员的输出并将其传递给编写者。最终结果是一个功能性的 MAS。它可以接收用户并并完成研究和写作过程。最后,我们通过在架构中添加错误处理和验证,使我们的原型更加健壮。
你现在已经从零开始设计、实现并稳定了一个 MAS。你可以使用这些高级工具构建下一代 AI 解决方案。我们的下一章将通过 RAG(检索增强生成)将 MAS 开放给外部数据,开启旅程的下一个阶段。
问题
-
这里的中心观点是一个大的 LLM 对复杂工作是最好的吗?(是或否)
-
协调器(
Orchestrator)执行实际的研究和写作吗?(是或否) -
MCP 仅仅是让 LLM 写得更好的花哨方式吗?(是或否)
-
研究员代理编写最终的博客文章吗?(是或否)
-
编写者直接从用户的第一个请求获取任务吗?(是或否)
-
你必须运行每个代理才能获得博客文章吗?(是或否)
-
每个 MCP 消息都必须包含发送者和内容吗?(是或否)
-
是否有一个所有代理共享的大提示词?(是或否)
-
笔记本中的代理通过互联网使用 HTTP 互相通信吗?(是或否)
-
这里的中心观点是一个大的 LLM 对复杂工作是最好的吗?(是或否)
参考文献
-
Model Context Protocol.
https://modelcontextprotocol.io -
Multiagent Systems: A Modern Approach.
-
JSON-RPC 2.0 Specification.
https://www.jsonrpc.org/specification -
ReAct: Synergizing Reasoning and Acting in Language Models.
arXiv:2210.03629 -
A Survey on Large Model Autonomous Agents.
https://arxiv.org/abs/2308.11432
构建上下文感知多智能体系统
-
为语义蓝图创建上下文库
-
使用向量存储命名空间(
namespaces)管理不同的数据类型 -
实现新的专家智能体,例如“上下文管理员”(
Context Librarian) -
编排整个系统以生成具有上下文感知的内容
我们将从探索 RAG MAS 架构开始。
设计双 RAG MAS 架构
为了构建我们的上下文感知系统,我们需要对其架构有一个清晰的认识。从高层来说,整个过程分为两个不同的阶段,我们将对其进行设计和开发。阶段 1 是数据准备,用于预处理数据;阶段 2 是运行时执行,我们的 MAS 使用准备好的数据来响应用户目标。本章将在两个笔记本中开发这两个阶段。
完整的双 RAG MAS 架构
阶段 1:数据准备(RAG 摄入)
阶段 2:运行时执行(MAS)
图 3.1:双 RAG MAS 架构
上图插插了双 RAG 架构的完整数据和执行流,箭头显示了组件在高层面上如何连接。在进入实际程序之前,我们将逐步介绍图中的每个部分。
阶段 1:数据准备
在阶段 1 中,我们重点关注数据准备,即图 3.1 的左侧部分。系统从两种不同的输入源开始:
-
知识数据(
Knowledge data):系统应该知道的事实信息。 -
上下文数据(
Context data):在第 1 章中引入的结构化指令或语义蓝图。两种数据类型都由嵌入模型处理。对于知识数据,我们将文本分割成块并对每个块进行嵌入;对于上下文数据,我们只对蓝图意图的描述进行嵌入。搜索基于意图。完整的蓝图内容单独存储在 JSON 对象中,并与其描述关联。
所有嵌入都存储在 Pinecone 数据库中,它将包含系统所需的所有内容。我们使用单个 Pinecone 索引,分为两个严格隔离的命名空间:
-
KnowledgeStore(知识库):此命名空间存储来自事实数据的向量。 -
ContextLibrary(上下文库):此命名空间存储来自程序化蓝图的向量。
两个命名空间就绪后,系统就准备好进入阶段 2。
阶段 2:运行时执行分析
在阶段 2 中,MAS 执行用户请求,如图 3.1 的右侧所示。
该过程从用户目标开始,这是用户提交的高级指令——例如,“写一个关于阿波罗 11 号的悬疑故事”。
编排器(Orchestrator)接收此目标。作为系统的中央协调器,它分析请求并识别出两个组件:
-
intent_query(意图查询):这是管理员(Librarian)查询的意图,与所需的样式结构相关(在我们的示例中是悬疑故事)。 -
topic_query(主题查询):这是研究员查询的主题,与主题内容相关(在我们的示例中是阿波罗 11 号)。
随后,编排器将每个查询委托给专家智能体:
-
管理员智能体(The
Librarianagent):负责检索指令脚本,通过引擎的 MCP 消息层接收intent_query。它查询ContextLibrary命名空间,执行语义搜索以找到匹配请求意图的蓝图描述,并检索相应的语义蓝图。 -
研究员智能体(The
Researcheragent):负责检索事实信息,接收topic_query。它查询KnowledgeStore命名空间,检索相关的事实块,并将其合成简洁的发现。
一旦两个响应都准备就,编排器现在就可以促进将检索到的指令脚本和合成的事实传递给作者(Writer)智能体。图 3.1 中的箭头显示了蓝图和事实从管理员和研究员向作者移动。
作者充当系统的生成引擎。它使用事实作为内容,并将蓝图作为对内容进行结构化和样式化的脚本指令。然后,作者生成最终输出,同时遵循事实要求和程序性约束。
这种架构允许高度的灵性和扩展性。可以在不更改指令脚本的情况下更新知识库,可以在不触碰事实的情况下添加新的蓝图。在运行时,智能体会根据它们检索到的上下文动态调整其行为。
解释架构后,我们现在可以进入实现。下一步是构建 RAG 摄入流水线。
RAG 流水线数据摄入(上下文与知识)
在第 2 章中,我们使用模拟数据构建了一个 MAS。智能体知道如何搜索,但它们受限于简单的 Python 字典。在本章中,我们迈出了下一步:用真实的 RAG 流水线取代该种模拟。
RAG 数据摄入流水线
图 3.2:显示了此流水线的流程。在智能体能够有效搜索之前,我们必须首先准备并构建它们将使用的数据库。我们的方法引入了两种互的形式的形式 RAG:
-
知识库(事实 RAG):存储事实。这是 RAG 的标准用法,大多数工程师都很熟悉。
-
上下文库(程序化 RAG):存储第一章引入的指令或语义蓝图。智能体查询此库以确定如何结构化它们的响应以及使用什么。
要实现这一点,请在 GitHub 中打开 RAG_Pipeline.ipynb 笔记本。
安装与设置
现在环境已就绪,让我们使用刚刚设置的 API 密钥连接到 OpenAI 和 Pinecone。对于本章,我们将此称为 genai-mas-mcp-ch3。
我们将检索 Pinecone API 密钥。在示例中,我正在使用 Google Colab 的密钥管理器:
PINEONE_API_KEY = userdata.get('PINEONE_API_KEY')
...
一旦获取了 API 密钥,我们需要为项目定义两个命名空间(namespaces)。这将确保我们的数据类型在 Pinecone 内部严格分离:
-
NAMESPACE_KNOWLEDGE将存储事实 -
NAMESPACE_CONTEXT将存储蓝图
我们正在使用 Pinecone 的无服务器架构(serverless architecture),这是 Pinecone 免费计划建议使用的。我们需要定期查看 Pinecone 的方案以确认条件是否有变化。对于本次演示,我将使用 us-east-1 的 AWS:
-- Initialize Clients --
client = OpenAI(api_key=OPENAI_API_KEY)
pc = Pinecone(api_key=PINEONE_API_KEY)
# Define Index and Namespaces --
INDEX_NAME = 'genai-mascp-ch3'
NAMESPACE_KNOWLEDGE = 'KnowledgeStore'
NAMESPACE_CONTEXT = 'ContextLibrary'
# Define Serverless Specification
spec = ServerlessSpec(cloud='aws', region='us-east-1')
在使用索引之前,我们需要检查它是否已经存在。如果不存在,代码将创建一个具有嵌入维度(embedding dimension)和余弦相似度(cosine similarity)指标的新索引,这是比较文本嵌入的常用选择:
## Check if index exists
if INDEX_NAME not in pc.list_indexes().names():
print(f"Index {INDEX_NAME} not found. Creating new serverless index...")
pc.create_index(
name=INDEX_NAME,
dimension=EMBEDDING_DIM,
metric='cosine',
spec=spec
)
## Wait for index to be ready
while not pc.describe_index(INDEX_NAME).status['ready']:
print("Waiting for index to be ready...")
第 3 章
time.sleep(1)
print("Index created successfully. It is new and empty.")
如果索引已经存在,我们可以选择是否清除现有数据。对于本次演示,我们将通过删除两个命名空间的内容每次都重新开始:
else:
print(f"Index {INDEX_NAME} already exists. Clearing namespaces for a fresh start...")
index = pc.index(INDEX_NAME)
namespaces_to_clear = [NAMESPACE_KNOWLEDGE, NAMESPACE_CONTEXT]
要清除索引,我们需要遍历每个命名空间并清除数据:
for namespace in namespaces_to_clear:
# Check if namespace exists and has vectors before deleting
stats = index.describe_index_stats()
if namespace in stats.namespaces and stats.namespaces[namespace].vector_count > 0:
print(f"clearing namespace {namespace}...")
index.delete(delete_all=True, namespace=namespace)
这里有一个棘手。清除函数可能是异步的,程序可能会运行得过快。这种缓慢的删除在我们开始上传后完成,这些新向量可能会被扫掉。这并不是绝对的,但确实是我们我们不想承担的风险。因此,我们等待系统并在向量计数真正为零后继续:
# CRITICAL FUNCTION: Wait for deletion to complete
while True:
stats = index.describe_index_stats()
if namespace not in stats.namespaces or stats.namespaces[namespace].vector_count == 0:
print(f"Namespace {namespace} cleared successfully.")
break
print(f"Waiting for namespace {namespace} to clear...")
time.sleep(5) # Poll every 5 seconds
else:
print(f"Namespace {namespace} is already empty or does not exist. Skipping.")
构建上下文感知多智能体系统
最后,我们准备好连接到索引进行后续操作:
# Connect to the index for subsequent operations
index = pc.Index(INDEX_NAME)
如果一切运行正常,你将看到指示索引已存在且命名空间已清除的输出:
Index genai-mas-cp-ch3 already exists. Clearing namespaces for a fresh start...
Namespace 'KnowledgeStore' is empty or does not exist. Skipping.
Namespace 'ContextLibrary' is empty or does not exist. Skipping.
在生产环境中,你可能不想在每次运行时都清除命名空间——你希望系统保留其存储的知识。然而,对于我们的学习之旅,每次重新开始更加整洁。索引就绪后,我们可以开始准备数据。
数据准备:上下文库(过程化 RAG)
在本节中,我们将为上下文库定义数据。这是我们过程化 RAG 实现的核心,因为我们正在引入过程化指令——而不仅仅是静态知识。上下文库建立在第一章语义蓝图概念的基础上,但现在我们将用精确的结构来实现它。
我们上下文库中的每个条目包含三个部分:
-
一个
id,它是 Pinecone 索引中的唯一标识符。 -
一个
description,用于解释蓝图的作用和风格。我们只对描述进行嵌入,以便为任务找到所需的蓝图。 -
蓝图包含 JSON 格式的 LLM(大语言模型)实际指令。蓝图不会被嵌入。一旦访问了描述,我们像在构建完一本书后一样检索蓝图……
第3章
83
输出确认我们已经定义了三个蓝图:
准备了 3 个上下文蓝图。
这里的要点是,创建语义蓝图既强大又具有挑战性。如果没有它们,你将不得不为每个 LLM 任务编写单独的代理(agent)。那样是不可扩展的。语义蓝图允许你一次捕获过程化指令并无限次复用。你不需要为几十个任务构建下拉菜单,而是依赖一个可扩展的自动化的上下文引擎。
定义好蓝图后,让我们转向双路 RAG 设置的另一侧:知识库。
数据准备:知识库(事实性)RAG
我们的任务是为知识库定义数据,即 RAG 设置中的事实部分。该组件持有相对静态的信息,研究员代理(Researcher agent)将搜索并将其合成简洁的发现。为了保持预期现实,你是在自己设计这个系统,而不是躲在华丽的平台背后;这需要大量细致的人工工作。
在这个教育示例中,我们将使用一个关于太空探索的小型数据集。它包含了太空竞赛和阿波罗 11 号、朱诺任务以及火星探测器。在本章中,我们将专注于架构,用数据字符串表示数据。让我们将知识整合到一个原始字符串中:
knowledge_data_raw = """
太空探索是利用天文学和太空技术探索外层空间。太空探索的早期阶段是由苏联和美国之间的“太空竞赛”驱动的。1957年苏联斯普尼克1号的发射以及1969年美国阿波罗11号首次登上月球是关键的里程碑。
朱诺号是 NASA 的一个探测木星的太空探测器。它于2011年8月5日发射,并于2016年7月5日进入木星极地轨道。
火星探测器是一种设计在火星表面行驶的遥控车辆。NASA JPL 管理了多个成功的探测器,包括:幸运者号、精神号、机遇号、好奇号和毅力号。寻找火星上宜居性和有机碳的证据现在是 NASA 的首要目标。毅力号还携带了“机智”直升机。
"""
现在我们将构建一个小型辅助函数,在嵌入之前将此文本处理成利于搜索的片段。
分块和嵌入的辅助函数
语言模型不处理原始段落——它们处理标记(tokens),这是模型将文本拆分为它可以理解的单位的方式。例如,单词 encoding 可能会被拆分成两个标记:enc 和 ing。模型使用这些部分来更好地理解单词关系。
我们将使用 c100k_base 分词器,这是 OpenAI 最新模型的标准配方。这种编码有三个优势:
-
它分割单词的方式非常聪明。它没有将像
encoding这样的单词视为整体,而是可能将其拆分为更小的、有意义的部分,如enc和ing。这有助于模型理解不同单词之间的关系。 -
它为速度而设计且运行高效,非常适合对话型模型以及为准备存储在数据库中的文本做准备。
-
使用它能确保你计数和格式化文本的方式与模型预期的完全匹配,这一点是至关重要的。
这种一致性至关重要。如果我们的分块(chunking)与嵌入模型的训练不匹配,语义搜索结果将是不准确的。这就像在一个图书馆里查书,但那里一半的卡片使用了不同的分类系统。
让我们初始化分词器:
# 初始化分词器以实现健壮的感知标记的分块
tokenizer = tiktoken.get_encoding("cl100k_base")
当信息分成足够的片段时,RAG 的效果最好。我们不是按字符数切割,而是按标记数切割,这对于 LLM 更加可靠。此外,程序定义了 400 token 的分块大小和 50 token 的重叠(overlap)。重叠有助于保持片段之间的上下文。你必须为你处理的每种数据找到最佳分块大小和重叠。
程序现在引入了 chunk_text 函数,确保长文档被拆分成整洁的、重叠的文本窗口,这些窗口更容易被模型处理且语义搜索更准确:
def chunk_text(text, chunk_size=400, overlap=50):
"""根据标记计数和重叠对文本分块(RAG 最佳实践)。"""
tokens = tokenizer.encode(text)
chunks = []
for i in range(0, len(tokens), chunk_size - overlap):
chunk_tokens = tokens[i:i + chunk_size]
chunk_text = tokenizer.decode(chunk_tokens)
# 基本清理
chunk_text = chunk_text.replace("\n", "").strip()
if chunk_text:
chunks.append(chunk_text)
return chunks
一旦我们有了片段,就需要将它们转换为嵌入。API 调用有时会失败(例如由于网络延迟),因此我们将添加一个重试机制。Tenacity 库中的 @retry 装饰器会自动使用指数退避重试失败的调用:
@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6)
)
我们并不上传原始片段。因此,我们需要定义 get_embeddings_batch 函数,将批量文本发送到 OpenAI API。它返回对应的嵌入:
@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6)
)
处理并上传 (upsert)
最后一步是将这些向量上传到 Pinecone。这是关键的。我们将分两部分处理:
-
上文库
-
知识库
上文库
一旦向量准备就绪,就可以上传了。这个过程被称为 upsert,即如果 ID 存在则更新,不存在则插入。我们使用 index.upsert 命令将向量到 NAMESPACE_CONTEXT:
# 数据
if vectors_context:
index.upsert(vectors=vectors_context, namespace=NAMESPACE_CONTEXT)
print(f"成功上传了 {len(vectors_context)} 个向量。")
如果运行成功,你会看到一个告知上传了多少向量的确认。此时,上下文库已进入命名空间,并准备供运行时查询。让我们进入 RAG 设置的另一部分:知识库。
知识库
现在让我们填充知识库,它将持有我们的事实数据。第一步是对原始字符串运行 chunk_text,以便将其拆分为更小、易管理的片段。这些片段随后将批量处理。例如,我们使用100的批量大小。程序遍历每个批量,使用 get_embeddings_batch 生成嵌入,并为 Pinecone 准备向量存储。每个片段都被赋予一个唯一的 ID,并与存储上下文的元数据配对。代码如下:
# 6.2. 知识库 ---
print(f"\n正在处理并上传知识库到命名空间:{NAMESPACE_KNOWLEDGE}")
# 分块知识数据
knowledge_chunks = chunk_text(knowledge_data_raw)
print(f"创建了 {len(knowledge_chunks)} 个知识片段。")
vectors_knowledge = []
batch_size = 100 # 分批量处理
for i in tqdm(range(0, len(knowledge_chunks), batch_size)):
batch_texts = knowledge_chunks[i:i+batch_size]
batch_embeddings = get_embeddings_batch(batch_texts)
for j, embedding in enumerate(batch_embeddings):
chunk_id = f"knowledge_chunk_{i+j}"
vectors_knowledge.append({
"id": chunk_id,
"values": embedding,
"metadata": {
"description": batch_texts[j],
"blueprint_json": knowledge_chunks[i+j]
}
})

浙公网安备 33010602011771号