在第 4 章中,我们构建了一个可运行的 Context Engine 原型——这是一个由一组专用智能体驱动、并由 LLM 驱动的 Planner 进行引导和调度的智能编排系统。由此产生的 notebook——Context_Engine.ipynb——验证了我们的架构蓝图,也证明了诸如动态规划(dynamic planning)和上下文链式传递(context chaining)这样的概念不仅在理论上成立,而且在实践中也确实可行。
然而,原型并不等同于可投入生产的系统。当前这套引擎虽然具有创新性,但还缺乏真实世界部署所要求的模块化能力与透明性。本章的重点,就是将这个实验性原型转化为一个面向业务、可用于预生产(pre-production)的框架。这个转化过程将分三个阶段展开。
我们首先会从架构层面深入分析,考察升级后引擎的顺序化结构。接着,我们进入组件强化(component hardening)阶段,使用诸如结构化日志(structured logging)、依赖注入(dependency injection)和错误处理(error handling)等生产级实践,来增强系统的每个部分。最后,我们会以模块化重构(refactoring for modularity)收尾,将原先那个单一、庞大的 notebook 改造成一个干净、可扩展、由可复用库组件构建而成的应用程序。
到本章结束时,Context Engine 将不再只是一个实验品。它将成为一个经过强化的、透明的、可扩展的系统,能够支撑复杂的多智能体工作流。本章将涵盖以下几个主要主题:
- 分步骤架构走查
- 面向可扩展性的重构
- 引擎模块化
分步骤架构走查
在我们开始组装即将在 Context_Engine_Pre_Production.ipynb 中构建的升级版 Context Engine 代码之前,先理解其内部机制至关重要。我们这次探索的向导,将是图 5.1。
图 5.1:升级版 Context Engine 流程图
图 5.1 追踪了一项任务如何从头到尾穿越整个系统。每个节点都带有颜色编码,并与 Context_Engine_MAS_MCP.ipynb 中相应函数所在的代码部分相对应,因此你可以沿着整条逻辑链,在代码库中清楚地看到它如何展开。该 notebook 同时也包含了我们将在“面向可扩展性的重构”一节中所做的升级内容,以及在验证过程中需要微调的部分:
- 第 7 节(蓝色) :用户最终运行的执行脚本
- 第 6 节(白色) :引擎核心,包括 orchestrator、planner 和 tracer
- 第 5 节(紫色) :Agent Registry,也就是系统的工具箱
- 第 4 节(绿色) :真正执行具体工作的专用智能体
- 第 3 节(橙色) :共享辅助函数,用于 LLM 调用、embedding 生成和向量数据库查询
引擎的运行会依次展开为四个彼此依赖、层层衔接的阶段:启动(Initiation) 、规划(Planning) 、执行(Execution)和收尾(Finalization) 。每个阶段在整体工作流中都承担着精确的角色,并将控制权传递给下一个阶段。这些阶段共同完成了从用户目标到具备上下文感知能力输出的转化。
请注意,在下面的走查过程中,括号中的章节编号与颜色,会与图 5.1 中的标注保持一致。
阶段 1:启动
每一次运行都始于用户的一次动作——也就是用户提交目标、引擎被启动的那一刻。
7.0a logging.info(蓝色) :系统执行的第一个动作,是记录整个过程的开始。这一步虽然简单,但在任何面向生产的系统中都至关重要,因为它能够用于追踪任务。
6.3b context_engine()(白色) :这是主入口,也是整个 orchestrator 的核心。用户目标会被传入这个函数,从这一刻开始,由它全面接管后续工作流。
阶段 2:规划阶段
一旦进入 context_engine(),第一件事就是制定策略。引擎不会盲目行动;它会先思考。它会收集关于“自己有哪些工具”“用户想要什么”“如何在两者之间建立桥梁”的信息。然后,它会起草出一份详细执行计划,供下游组件遵循。
6.1a ExecutionTrace.__init__()(白色) :引擎会立刻初始化一个 ExecutionTrace 对象。这个对象就像引擎的“飞行记录仪”,从此刻开始准备记录后续每一个动作,以支撑透明性和调试。
5.3 AgentRegistry.get_capabilities_description()(紫色) :为了生成一份有效计划,Planner 必须先知道自己手头有哪些工具。因此,它会查询 AGENT_TOOLKIT 对象,获取关于所有专用智能体及其所需输入的纯文本描述。
6.2 planner()(白色) :在拿到用户目标和智能体能力清单之后,引擎调用 planner() 函数。
3.1 call_llm_robust()(橙色) :planner() 会把推理任务委派给外部 LLM。它会把目标和能力描述组织成一个精心设计的 prompt,然后要求 LLM 返回一份战略性的、多步骤 JSON 计划。
6.1b ExecutionTrace.log_plan()(白色) :一旦从 LLM 收到 JSON 计划,它就会立即被记录到 ExecutionTrace 对象中。至此,规划阶段结束。
阶段 3:执行循环
这正是 Context Engine 真正“活起来”的地方。有了计划之后,系统开始按照严格顺序执行每一个步骤(这里的 step 指的是 Context Engine 动态流程中真实存在的、编号化的步骤)。它会为当前任务检索正确的智能体,准备好输入,并追踪每一个输出,从而形成一个持续的“规划—行动—验证”反馈回路。
循环从 Executor 读取计划中的下一个步骤开始:
5.2 AgentRegistry.get_handler()(紫色) :计划中会按名称指定一个智能体(例如 "Librarian")。Executor 使用 get_handler() 从注册表中取出真正可调用的 Python 函数(例如 agent_context_librarian)。
6.3a resolve_dependencies()(白色) :这是上下文链式传递的核心。Executor 会检查该智能体需要的输入。如果发现类似 $$STEP_1_OUTPUT$$ 的占位符,这个函数就会把它替换成第 1 步实际产生的数据,而这些数据存储在引擎的状态字典中。
4.x agent_*()(绿色) :然后,Executor 会调用刚刚检索到的智能体函数,并将已经解析完成的上下文传入。
在每个智能体内部,专门化逻辑会驱动系统的问题求解行为。这些智能体通常依赖辅助函数来访问外部资源,并承担真正的“重活”:
- Librarian 和 Researcher 智能体会调用 3.4
query_pinecone()(橙色) ,对向量数据库执行语义检索。这个函数本身又会调用 3.2get_embedding()(橙色) ,将文本查询转化为向量。 - Researcher 和 Writer 智能体会调用 3.1
call_llm_robust()(橙色) ,分别用于综合事实或生成最终内容。
6.1c ExecutionTrace.log_step()(白色) :一旦智能体完成工作并返回输出,就会调用 log_step() 方法,记录关于该步骤的一切信息:使用了哪个智能体、输入是什么、最终输出是什么。输出同时也会被保存到引擎状态中,以供后续步骤使用。
随后,循环进入计划中的下一个步骤,重复执行“检索智能体—准备输入—执行步骤—记录结果”的全过程。
阶段 4:收尾
最后一个阶段负责闭环。所有步骤执行成功之后,Context Engine 会通过记录最终状态、返回结果和完整 trace 来结束任务。这个阶段强调的是一种平稳、优雅的完成方式:既交还一个清晰结果,也交还一份透明记录,说明这个结果是如何达成的。
6.1d ExecutionTrace.finalize()(白色) :引擎会在 trace 对象上调用 finalize(),记录最终状态(“Success”)以及总执行时间。
7:然后 context_engine() 会将最终输出(也就是最后一步的结果)和完整 trace 对象一起返回给用户脚本。
7.0b logging.info 与 7.1/7.2 display(Markdown(...))(蓝色) :脚本会记录任务已完成,并将最终格式化后的输出展示给用户,从而成功满足最初的目标。
在前面的流程图中,每一个颜色块都代表一个职责单一、边界清晰的组件。它们共同构成了一套干净的分层架构,而这正是可扩展多智能体系统的核心特征:
- 引擎核心(白色) 是大脑。它自己不直接做具体工作,但会管理整个流程。它负责制定策略(planner)、逐步执行计划,并记录一切(
ExecutionTrace)。其中的主函数context_engine()就是总编排器。 - Agent Registry(紫色) 是工具箱。它同时服务于两个角色:它向 Planner 提供说明文档(
get_capabilities_description),让 Planner 能“想”;它向 Executor 提供真正的工具(get_handler),让 Executor 能“做”。 - 专用智能体(绿色) 是工人。每个智能体只负责一种具体任务:Librarian 负责找风格指令,Researcher 负责找事实,Writer 负责把它们融合成内容。它们就是整个操作的“手”。
- 辅助函数(橙色) 是公用工具。它们是任何其他组件都可调用的底层共享函数,用于处理那些重复但必不可少的任务,比如与 LLM 通信(
call_llm_robust)和与向量数据库交互(query_pinecone)。它们防止代码重复,并集中管理关键交互。
通过分析这套架构,我们可以看到,这个系统是如何通过一系列逻辑清晰、可预测、可追踪的简单操作,来实现复杂行为的。既然我们已经详细理解了这张蓝图,现在就可以开始用代码去构建这些组件了。
面向真实世界可扩展性的重构
将原型转化为可投入生产的应用,我们的第一步是软件工程中至关重要的一步:重构(refactoring) 。在第 4 章中,我们的 notebook 里有多个单元格专门负责安装依赖包、初始化 API 客户端,以及辅助函数、智能体和 Context Engine 的代码。虽然这种方式能够工作,但它有两个明显缺点:其一,它会让主工作区被大量底层初始化任务弄得杂乱无章;其二,它迫使我们在每一个新 notebook 中都重复同样的代码。
为了解决这个问题,我们现在要把这些基础逻辑迁移到新的 utils.py 文件中。你可以把这一步理解为:从一个凌乱的工作台,走向一个专业而有组织的工坊。我们不再让工具和钥匙散落各处,而是创建一个集中化的“初始化清单”,只需一个命令就能运行。这不仅能让我们的主 notebook 更加清爽,使我们能够把注意力放在引擎高层编排逻辑上,而且还能确保项目中每个组件都建立在一致且可靠的基础之上。与此同时,我们还会讨论在验证运行(validation runs)过程中出现的真实问题,以及我们是如何解决这些问题的。
第 4 章验证的是 Context Engine 的逻辑;而这一节要做的,是把这个可运行的概念转化为一个可维护的系统。我们将从一个以 notebook 为中心的单体原型,演进为一个模块化、依赖注入化、并且具备完整可观测能力的代码库,使团队能够稳定复现它。
现在,请打开 Context_Engine_Pre_Production.ipynb,让我们开始把这个 context engine 改造成一个预生产系统。
创建集中化的初始化函数
第一步,是在 utils.py 文件中创建两个专用函数:一个负责安装依赖,另一个负责初始化客户端。
install_dependencies() 函数可以看作项目的“一键安装器”。通过把所有 pip 命令封装进这一个函数,我们能够确保项目中的每个 notebook 都使用完全相同版本的依赖库。这一点对于保证可复现性至关重要,也能避免复杂项目中常见的依赖冲突问题。
initialize_clients() 函数则充当我们的“安全密钥管理员”。它统一负责从 Google Colab Secrets 中安全获取 API_KEY 和 PINECONE_API_KEY,并利用这些密钥创建和配置 OpenAI 与 Pinecone 的客户端对象。
这两个函数共同构成了一套可复用且安全的初始化流程,使得项目中的每一个组件都能从一致、可靠的环境中启动。
简化主 notebook
当我们的初始化逻辑被整齐地安置在 utils.py 之后,主 notebook——Context_Engine_Pre_Production.ipynb——就可以被大幅简化。原先那些冗长的安装与客户端设置单元格,现在被一个优雅的单元格所取代,只需几行代码,就能让环境准备就绪。
我们首先导入新的工具模块,然后直接调用刚才创建的两个初始化函数。这种抽象方式,正是专业软件设计的精髓所在。主 notebook 不再需要关心初始化细节;它只需要知道:这些事情必须完成。这样一来,我们就拥有了一个干净、可读的工作区,能够把注意力集中到升级引擎核心逻辑这类更高阶任务上。
有了这个基础之后,我们就可以进入下一步:强化辅助函数,因为它们构成了 Context Engine 的通信骨架。
强化辅助函数
随着项目结构完成重构,现在我们要把注意力转向引擎的“中枢神经系统”:辅助函数。这些工具函数——call_llm_robust、get_embedding 和 query_pinecone——是引擎大脑与外部工具之间的关键通信线路。在第 4 章中,它们已经很好地完成了自己的任务,但它们依赖全局变量来获取 API 客户端、模型名称等关键组件。随着我们朝着生产级系统迈进,这种方式显得过于僵化,也更容易出错。这些辅助函数必须被强化。
本节中,我们会对这些核心函数进行升级,让它们更可靠、更可测试、更透明。最关键的架构转变,是引入依赖注入(dependency injection) 。与其让函数自己去抓取全局变量,我们现在会显式地把所有需要的组件(例如客户端对象和配置项)作为参数传入函数。这样一来,这些函数就从“简单连接器”变成了“有韧性、可自洽的模块”——它们能够优雅处理故障,给出清晰日志,并更有效地管理资源。现在就开始吧。
用依赖注入增强模块化
对辅助函数最重大的升级,就是摆脱全局变量。在原型中,像 call_llm_robust 这样的函数会“隐式知道”自己该用哪个 client 和 GENERATION_MODEL,因为这些值定义在全局作用域中。对于原型而言,这样做很方便;但对真实应用来说,这种设计是危险的,因为它制造了隐藏依赖,让代码难以测试,也难以重配置。
我们的升级引入了依赖注入:也就是把函数所需依赖通过参数“注入”进来。这让每个函数都变成一个自洽单元。只要调用时提供了 client 或 model name,它们究竟来自哪里,函数本身并不关心。这大幅提升了引擎的灵活性。例如,我们甚至可以同时运行两个不同的引擎任务,并分别使用两个不同的 LLM 提供方——只需要传入不同的 client 对象即可。
用生产级日志增强透明性
在原型中,我们使用 print() 来观察程序运行情况。对于生产系统而言,这远远不够。一个专业系统需要正式的日志:结构化的、带时间戳的、可靠的事件记录。
因此,我们最后一项增强,就是把所有 print() 替换为 Python 内置的 logging 模块。这是一个根本性的升级,它提升了系统的透明性。规范的日志系统让我们能够区分常规的 INFO 消息、潜在警告,以及关键错误。这种结构化输出不仅更方便人类阅读,也具备机器可读性,因此能够被自动监控和告警系统消费。
# === Configure Production-Level Logging ===
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
用主动上下文管理提升效率
构建 LLM 应用时,一个关键问题是管理 token 的“隐形成本”。你发送给模型的每一段上下文都会消耗 token,这会影响成本和速度;一旦超出上下文窗口上限,还可能直接导致硬性失败。一个专业系统绝不能对这些资源掉以轻心。
为了解决这个问题,我们引入了 count_tokens 工具函数。它就像一个“油量表”,能让引擎在把 prompt 发送给模型之前,先测量其 token 成本。它使用 OpenAI 官方的 tiktoken 库来保证准确性。通过现在就把这一步打好基础,我们也为之后更高级的特性铺平了道路,例如自动压缩或总结上下文,以保持在 token 预算范围之内。
# === Context Management Utility (New) ===
def count_tokens(text, model="gpt-4"):
"""Counts the number of tokens in a text string for a given model."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
# Fallback for models that might not be in the tiktoken registry
encoding = tiktoken.get_encoding("cl100k_base")
return len(encoding.encode(text))
这个简单函数,是让引擎不仅强大,而且“有成本意识、讲效率”的关键第一步。
升级后的辅助函数实战
下面是核心辅助函数最终强化后的代码。请注意,每个函数签名现在都显式包含了诸如 client、generation_model 和 embedding_model 等参数。所有的 print 语句都被替换成了 logging 调用,并且错误处理也更具针对性。
# === LLM Interaction (Hardened with Dependency Injection) ===
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def call_llm_robust(
system_prompt, user_prompt, client, generation_model,
json_mode=False
):
"""
A centralized function to handle all LLM interactions with retries.
UPGRADE: Now requires the 'client' and 'generation_model' objects to be passed in.
"""
logging.info("Attempting to call LLM...")
try:
response_format = {"type": "json_object"}
if json_mode else {"type": "text"}
# UPGRADE: Uses the passed-in client and model name for the API call.
response = client.chat.completions.create(
model=generation_model,
response_format=response_format,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
)
logging.info("LLM call successful.")
return response.choices[0].message.content.strip()
except APIError as e:
logging.error(f"OpenAI API Error in call_llm_robust: {e}")
raise e
except Exception as e:
logging.error(f"An unexpected error occurred in call_llm_robust: {e}")
raise e
# === Embeddings (Hardened with Dependency Injection) ===
@retry(wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6)
)
def get_embedding(text, client, embedding_model):
"""
Generates embeddings for a single text query with retries.
UPGRADE: Now requires the 'client' and 'embedding_model' objects.
"""
text = text.replace("\n", " ")
try:
# UPGRADE: Uses the passed-in client and model name.
response = client.embeddings.create(input=[text], model=embedding_model)
return response.data[0].embedding
except APIError as e:
logging.error(f"OpenAI API Error in get_embedding: {e}")
raise e
except Exception as e:
logging.error(f"An unexpected error occurred in get_embedding: {e}")
raise e
通过先捕获并记录一个具体的 API 错误,然后再重新抛出异常(raise e),我们实现了“两全其美”。一方面,函数会把详细而具体的错误信息写入日志;另一方面,它也会把失败信号向上传递到主 context_engine 函数。这使得引擎能够做出战略性决策:终止整个计划。这是一个至关重要的安全特性,因为它能防止系统在信息不完整的情况下继续执行。
有了这些强化后的辅助函数,引擎的核心通信层就已经变得模块化且透明,也为支撑专用智能体中更复杂的逻辑做好了准备。
面向生产重构智能体
为了完整展示每个智能体的演化过程,本节将追踪它们从 Context_Engine_MAS_MCP.ipynb 中初步强化,到最终模块化进 agents.py 库文件的全过程。这样安排,可以让我们在同一个地方看清每个智能体的演进路径,以及它们在真实调试过程中遇到的问题;之后我们才会在“引擎模块化”一节中正式组装这些最终库文件。
在这个阶段,我们不再依赖全局变量,而是将所有依赖——例如 API 客户端和模型配置——都通过参数直接传给智能体。借助依赖注入,系统变得更易测试、更易扩展、更易维护。与此同时,我们也把所有内部通信从简单的 print() 升级为结构化日志,这正是生产级环境所要求的透明性。
这些改动共同把智能体从“简单的任务函数”升级为“面向业务、可审计的应用组件”。下面我们继续看看,我们是如何在 Context_Engine_MAS_MCP.ipynb 中重构代码,并将升级后的版本适配到 Context_Engine_Pre_Production.ipynb 的部署环境中的,这也为之后“引擎模块化”一节中的整合打下了基础。
Context Librarian 智能体
我们首先升级的是 Context Librarian。我们将把它从第 4 章中构建的简单原型,转变成一个经过强化、面向生产的组件。最初的升级发生在 Context_Engine_MAS_MCP.ipynb 中,重点是引入依赖注入与专业级日志。
这个智能体的重构,是一个非常典型的“生产强化”案例。我们会先查看在 notebook 中做的第一轮升级,然后展示:当我们把这段代码迁移进最终的 agents.py 文件时,如何暴露出一个细微但重要的缺陷,以及我们是如何修复它、从而得到最终生产级版本的。
正如我们将看到的那样,第一轮修改在最终版本执行时暴露出了一个关键问题。需要认识到,这类问题是 AI 智能体开发中的自然组成部分,而能否系统化地解决这些问题,正是一个系统能否具备韧性的关键。以下是中间实现中的关键代码行,展示了升级后的函数签名和最初有缺陷的返回语句:
# === 4.1. Context Librarian Agent (Upgraded) ===
# *** Added 'namespace_context' argument ***
def agent_context_librarian(
mcp_message, client, index, embedding_model, namespace_context
):
# ... (logic for querying Pinecone) ...
if results:
# ...
content = blueprint_json # FLAW: content is a raw string
# ...
return create_mcp_message("Librarian", content)
我们在这一阶段完成了以下改进:
- 通过依赖注入增强了函数设计,使其签名中显式要求所有外部依赖:
client、index、embedding_model和namespace_context。这让智能体成为一个自洽、可测试的单元。 - 我们将所有
print语句替换为专业的日志调用。 - 我们增强了错误处理机制。整个逻辑被包裹在
try...except中,以便优雅处理故障。
在这一过程中,旧版本中的问题变得十分明显:它把蓝图直接作为一个原始 JSON 字符串返回。
但如果整个系统要具备鲁棒性,每个智能体都必须以一种可预测、结构化的方式返回数据。例如,Writer 智能体必须清楚地知道自己应该从 Librarian 的输出中读取哪个 key。修复方式,就是把输出包裹进一个带有固定 key——"blueprint_json"——的字典中。这样就在智能体之间建立了一份稳定的数据契约(data contract)。
最终修正后的代码,如今已位于 agents.py 中,也正体现了这一关键修复以及其他生产级增强。
最终强化后的代码
现在我们来看看最终版本的代码,并逐步解释每一项升级落在了哪里。首先,我们定义强化后的函数签名。现在所有依赖都作为显式参数传入,而不是依赖全局变量,这使得该智能体成为一个自包含单元:
def agent_context_librarian(
mcp_message, client, index, embedding_model, namespace_context
):
"""Retrieves the appropriate Semantic Blueprint from the Context Library."""
接着,我们用专业日志替换旧的 print(),并将整个函数包裹在 try...except 中,以实现稳健的错误处理:
logging.info("[Librarian] Activated. Analyzing intent...")
try:
requested_intent = mcp_message['content'].get('intent_query')
if not requested_intent:
raise ValueError("Librarian requires 'intent_query' in the input content.")
对 query_pinecone 辅助函数的调用也升级为显式关键字参数形式,并把通过依赖注入获得的 client 和 model 对象完整传下去:
results = query_pinecone(
query_text=requested_intent,
namespace=namespace_context,
top_k=1,
index=index,
client=client,
embedding_model=embedding_model
)
最后,我们实现了在验证阶段发现的最关键修复。智能体的输出现在是一个带有固定 key 的字典,从而为其他智能体建立了可靠的数据契约:
if results:
match = results[0]
logging.info(
f"[Librarian] Found blueprint '{match['id']}' (Score: {match['score']:.2f})"
)
blueprint_json = match['metadata']['blueprint_json']
content = {"blueprint_json": blueprint_json}
else:
logging.warning("[Librarian] No specific blueprint found. Returning default.")
content = {"blueprint_json": json.dumps(
{"instruction": "Generate the content neutrally."}
)
}
return create_mcp_message("Librarian", content)
这个小改动正体现了重构的本质:它不仅仅是“把代码挪个地方”,更是让代码随着系统增长而变得更可预测、更易维护。
Researcher 智能体
与 Librarian 的改造路径类似,Researcher 也经历了同样的渐进式提升:先在 notebook 中强化,再迁移进库文件。整个过程从 Context_Engine_MAS_MCP.ipynb 中引入依赖注入和结构化日志开始,随后再模块化到 agents.py 中。
和 Librarian 一样,Researcher 的第一版也暴露出了一个细微但关键的缺陷:在验证阶段,我们发现它返回的是原始字符串,而不是结构化数据。这个错误看起来很容易犯,但它会在整个工作流中层层传递并放大:
# === 4.2. Researcher Agent (Upgraded) ===
def agent_researcher(
mcp_message, client, index, generation_model, embedding_model,
namespace_knowledge
):
# ... (logic for querying Pinecone and synthesizing) ...
if not results:
return create_mcp_message("Researcher", "No data found on the topic.") # FLAW: returns a raw string
# ...
findings = call_llm_robust(...)
return create_mcp_message("Researcher", findings) # FLAW: returns a raw string
这一阶段的升级,在结构上与 Librarian 的改造类似:
- 通过显式要求所有外部依赖——
client、index、generation_model、embedding_model和namespace_knowledge——强化依赖注入; - 用结构化日志替换
print(),满足生产级透明性要求; - 用
try...except包裹逻辑,实现更稳健的错误处理。
在 notebook 内完成这些强化之后,下一步就是把它迁移进 agents.py 库文件。也正是在这个过程中,问题变得十分明确:旧版本返回的是原始 JSON 字符串。
但要让系统具备鲁棒性,智能体之间必须通过一种可预测、结构化的格式进行通信。Writer 智能体必须明确知道应该从 Researcher 的输出中读哪个 key 才能拿到事实信息。解决方案就是:把输出包装进一个带有固定 key——"facts"——的字典里。这就为智能体之间建立起了一份稳定的数据契约。
最终修正后的代码,如今出现在 agents.py 中,也体现了这项关键修复以及其他专业级增强。
最终强化后的代码
先来看最终版的函数签名。现在,所有依赖都通过显式参数传入,而不是依赖全局变量:
def agent_researcher(
mcp_message, client, index, generation_model, embedding_model,
namespace_knowledge
):
"""Retrieves and synthesizes factual information from the Knowledge Base."""
然后,我们用专业日志替换旧的 print,并用 try...except 对整个函数进行包裹,以实现稳健的错误处理:
logging.info("[Researcher] Activated. Investigating topic...")
try:
topic = mcp_message['content'].get('topic_query')
if not topic:
raise ValueError("Researcher requires 'topic_query' in the input content.")
对 query_pinecone 的调用也已经升级。我们使用显式关键字参数,并把所有通过依赖注入获得的 client 和 model 对象传给它:
results = query_pinecone(
query_text=topic,
namespace=namespace_knowledge,
top_k=3,
index=index,
client=client,
embedding_model=embedding_model
)
最后,最关键的修复也在这里实现了:智能体输出现在是一个带有固定 "facts" key 的字典,从而为其他智能体建立了稳定的数据契约:
if not results:
logging.warning("[Researcher] No relevant information found.")
return create_mcp_message("Researcher", {"facts": "No data found on the topic."})
logging.info(f"[Researcher] Found {len(results)} relevant chunks. Synthesizing...")
source_texts = [match['metadata']['text'] for match in results]
system_prompt = """You are an expert research synthesis AI.
Synthesize the provided source texts into a concise, bullet-pointed summary answering the user's topic."""
user_prompt = f"Topic: {topic}\n\nSources:\n" + "\n\n---\n\n".join(source_texts)
findings = call_llm_robust(
system_prompt,
user_prompt,
client=client,
generation_model=generation_model
)
return create_mcp_message("Researcher", {"facts": findings})
通过这种方式封装输出,我们不仅稳定了数据流,还在整个智能体生态中强制执行了一致性。正如接下来会看到的,作为最后一位专家的 Writer,也沿着相同的演化路径前进。
Writer 智能体
Writer 智能体补齐了我们三位专用智能体的最后一块拼图,而它的重构也展示了整个系统最终整合时最关键的一课。和 Librarian、Researcher 一样,它也先在 Context_Engine_MAS_MCP.ipynb 中完成了结构化日志、错误处理和依赖注入等强化。
不过,这次升级揭示了一个关于集成系统最重要的经验:你改动了一个组件,就必须同步更新所有依赖它的组件。 从设计走向完美从来都不是一条直路,因此,知道如何解决问题,和知道如何开发函数本身一样重要。Writer 的初版强化代码可以和旧版 Librarian、Researcher 配合工作,但它包含一个关键缺陷:在面对它们的新版本时,它无法正常工作。
下面这几行中间实现中的关键代码,展示了它在输入处理上的缺陷:
# === 4.3. Writer Agent (Upgraded) ===
def agent_writer(mcp_message, client, generation_model):
"""Combines research with a blueprint to generate the final output."""
logging.info("[Writer] Activated. Applying blueprint to source material...")
try:
# --- FIX: Unpack the structured inputs from previous steps ---
blueprint_data = mcp_message['content'].get('blueprint')
facts_data = mcp_message['content'].get('facts')
previous_content_data = mcp_message['content'].get(
'previous_content'
)
…
先来看一下最初做过的升级:
- 函数签名已经强化,显式要求
client和generation_model,让它成为一个自包含单元; - 所有
print语句被替换成专业日志; - 整个逻辑被包裹进
try...except中,以便优雅处理故障。
现在我们来看看这个整合层面的缺陷是如何修复的。
问题出在 Writer 对输入的理解方式上。它原本期望拿到的是原始字符串,而不是带有 "blueprint_json" 和 "facts" key 的结构化字典。为了解决这个问题,我们升级了 Writer,让它能够智能地拆包这些结构。这不仅仅是一个 bug 修复,更是构建真正互联、真正鲁棒的多智能体系统的最后一步:让数据能够在组件之间以可预测的方式流动。最终修正后的版本,如今出现在 agents.py 中,也正体现了这一关键整合修复。
最终强化后的代码
首先是强化后的函数签名和初始日志设置:
def agent_writer(mcp_message, client, generation_model):
"""Combines research with a blueprint to generate the final output."""
logging.info("[Writer] Activated. Applying blueprint to source material...")
try:
接着,我们实现最关键的修复。这段新代码会智能地解包结构化输入:如果输入是来自新版本智能体的字典,它就从中提取真实字符串;如果输入本身就是原始字符串,它也能兼容。这让智能体具备了鲁棒性:
blueprint_data = mcp_message['content'].get('blueprint')
facts_data = mcp_message['content'].get('facts')
previous_content_data = mcp_message['content'].get(
'previous_content'
)
# Extract the actual strings, handling both dict and raw string inputs
blueprint_json_string = blueprint_data.get('blueprint_json')
if isinstance(blueprint_data, dict) else blueprint_data
facts = facts_data.get('facts')
if isinstance(facts_data, dict) else facts_data
previous_content = previous_content_data # Assuming this is already a string if provided
if not blueprint_json_string:
raise ValueError("Writer requires 'blueprint' in the input content.")
之后,智能体的其余逻辑保持不变,但现在它运行在已经被正确解包的 facts 和 blueprint_json_string 变量之上:
if facts:
source_material = facts
source_label = "RESEARCH FINDINGS"
elif previous_content:
source_material = previous_content
source_label = "PREVIOUS CONTENT (For Rewriting)"
else:
raise ValueError("Writer requires either 'facts' or 'previous_content'.")
# ... (System and User prompt construction remains the same) ...
最后,Writer 通过依赖注入方式调用 LLM,并返回输出;此时它已经能够确信,自己正确解释了前序智能体传来的数据:
final_output = call_llm_robust(
system_prompt,
user_prompt,
client=client,
generation_model=generation_model
)
return create_mcp_message("Writer", final_output)
except Exception as e:
logging.error(f"[Writer] An error occurred: {e}")
raise e
到这里,整支专用智能体团队都已经达到了生产级标准。接下来,我们将重构负责管理它们的组件。
重构 Agent Registry
现在我们把目光转向 AgentRegistry——它是引擎里的“工头”,相当于手中拿着总控清单的那个人。在第 4 章中,它只是一个简单的名册:把智能体名称映射到对应函数。现在,我们要把它升级为一个智能化的中央工具箱。
这次升级,是整个组件强化过程中最后也是最关键的一步,因为它负责正确地管理并向所有专用智能体注入依赖。和前面其他组件一样,我们会先在 Context_Engine_MAS_MCP.ipynb 中升级它。第一轮升级主要围绕 get_handler 方法展开:让它能够管理强化后智能体新增的依赖(例如 client、index 和 namespace 配置等)。不过,和之前的重构一样,第一轮改动也暴露出一个根本性的架构缺陷。notebook 版本里,注册表直接引用智能体函数——这在单文件环境里是个捷径,但一旦模块化,就会出问题:
class AgentRegistry:
def __init__(self):
self.registry = {
# ADD the "agents." prefix to each function name
"Librarian": agent_context_librarian,
"Researcher": agent_researcher,
"Writer": agent_writer,
}
# ...
在这个阶段,get_handler 方法其实已经得到了显著增强。它的签名现在接收所有关键依赖(client、index、generation_model 等),并通过 lambda 动态生成“可直接调用的 handler”,同时只向对应智能体注入它真正需要的依赖。
但一旦这个 registry 被迁移到独立的 registry.py 文件中,一个熟悉的模式又出现了。就像我们在模块化智能体时看到的那样,把代码移出 notebook 之后,它原来的“共享作用域”就消失了:每个文件都是自己的环境。原始代码默认认为智能体函数存在于同一个全局命名空间中,这就导致重构后的模块直接报出 NameError。
解决方式,与修复 Librarian 和 Researcher 时采用的思路一样:让 registry 本身变成一个自包含模块。它必须显式导入 agents 模块,并通过正确命名空间来引用函数,例如 agents.agent_context_librarian。这个小改动虽然不起眼,却至关重要。它确保每个组件都明确知道自己的依赖位于何处,从而建立起一个真正模块化的架构。
registry.py 中最终修正后的代码,正体现了这一改进。
最终强化后的代码
先来看第一步:registry 正确导入自己的依赖:
import logging
import agents
接着,在 __init__ 方法内部,它通过 agents. 前缀来引用各智能体函数,修复了模块化层面的缺陷:
class AgentRegistry:
def __init__(self):
self.registry = {
# Add the "agents." prefix to each function name
"Librarian": agents.agent_context_librarian,
"Researcher": agents.agent_researcher,
"Writer": agents.agent_writer,
}
get_handler 方法本身,仍然是我们在 notebook 中构建出的那个强大的依赖注入器,只不过现在它是基于已正确导入的智能体函数来工作的:
def get_handler(
self, agent_name, client, index, generation_model,
embedding_model, namespace_context, namespace_knowledge
):
handler_func = self.registry.get(agent_name)
if not handler_func:
logging.error(f"Agent '{agent_name}' not found in registry.")
raise ValueError(f"Agent '{agent_name}' not found in registry.")
if agent_name == "Librarian":
return lambda mcp_message: handler_func(
mcp_message, client=client, index=index,
embedding_model=embedding_model,
namespace_context=namespace_context
)
elif agent_name == "Researcher":
return lambda mcp_message: handler_func(
mcp_message, client=client, index=index,
generation_model=generation_model,
embedding_model=embedding_model,
namespace_knowledge=namespace_knowledge
)
elif agent_name == "Writer":
return lambda mcp_message: handler_func(
mcp_message, client=client,
generation_model=generation_model
)
else:
return handler_func
从设计,到开发,再到修复缺陷,这一轮确实走得不轻松。接下来,让我们继续升级 Context Engine 本身。
升级中央编排器
随着智能体与注册表都已经被强化并完成模块化,现在我们把注意力转向整个架构的核心:Context Engine 本身。它就像系统的中央大脑,将所有独立组件整合成一台协同思考的机器。
在第 4 章中,我们已经构建了驱动这一编排循环的核心元素。本节中,我们会把这些元素升级到新的生产级标准。Context Engine 是整个应用的中枢神经系统,其中包含三个关键组件:ExecutionTrace 类(Tracer)、planner 函数,以及 context_engine 编排器。
把它从 Context_Engine_MAS_MCP.ipynb 迁移到专门的 engine.py 文件中,是我们迈向模块化预生产系统的最后一步,也是最关键的一步。虽然某些单独的类(例如 ExecutionTrace)在 notebook 中已经很稳健,但把它们黏在一起的“单体结构”本身,依然是一个严重缺陷。若想让引擎具备可复用性和可维护性,它的逻辑就必须彻底从 notebook 环境中解耦。
和其他组件一样,一旦我们从单 notebook 原型走向模块化代码库,那些隐式依赖就会被暴露出来。notebook 版本的 Context Engine 默认假设所有支持模块(agents、helpers、registry)都存在于同一个作用域中。一旦把引擎迁移到独立文件里,这种假设就会失效:
class ExecutionTrace:
# ... (Tracer logic is already robust) ...
def planner(goal, capabilities):
# ...
def context_engine(goal, ...):
# FLAW: This function directly calls other functions like planner()
# and uses the AGENT_TOOLKIT, assuming they are globally available.
因此,我们采用和智能体、注册表相同的方式,把引擎迁移进自己的文件——engine.py。这种模块化不仅修复了架构缺陷,也让 Context Engine 成为一个自包含组件:它显式导入所有自己依赖的内容。
现在,engine.py 会导入 helpers 和 registry(而 registry 又会导入 agents)。结果就是:我们得到一个独立的 engine,它既可以被导入到任意 notebook 中使用,也可以直接被集成进更大的应用程序中,从而真正达成“预生产、可复用系统”的目标。
下面展示的最终修正版代码,代表了一整套端到端的编排层,它把所有组件完整地绑定在了一起。
最终强化后的代码
首先,是模块化导入,这一步修复了单体 notebook 的核心架构缺陷:
…
import logging
import time
import json
import copy
from helpers import call_llm_robust, create_mcp_message
from registry import AGENT_TOOLKIT
接着是 ExecutionTrace 类。它的逻辑本身已经足够稳健,因此被原样迁移进模块:
class ExecutionTrace:
"""Logs the entire execution flow for debugging and analysis."""
# ... (Full class code as provided) ...
然后是 planner 函数。它作为战略核心,借助 LLM 生成逐步执行计划:
def planner(goal, capabilities, client, generation_model):
"""Analyzes the goal and generates a structured Execution Plan using the LLM."""
# ... (Full planner logic as provided) ...
最后,context_engine 函数作为主 orchestrator,也可以视作 Executor。它会初始化 trace,调用 planner,然后遍历计划中的每一步,通过 AGENT_TOOLKIT 获取正确的智能体 handler 并执行任务。这正是把整份计划真正“带活”的引擎核心:
def context_engine(
goal, client, pc, index_name, generation_model, embedding_model,
namespace_context, namespace_knowledge
):
"""The main entry point for the Context Engine."""
# ... (Full execution loop logic as provided) ...
至此,引擎的大脑已经彻底升级完成,整个框架也已经被强化并准备就绪。本章最后一步,就是用一次真实运行来测试这套重构后的系统。
运行强化后的引擎
到现在为止,我们已经成功重构并强化了 Context Engine 的每一个组件。我们增强了辅助函数的可靠性,通过依赖注入将智能体模块化,构建了一个智能化的 Agent Registry,并将核心逻辑统一收束进 context_engine 这个 orchestrator 中。现在,见证结果的时刻到了。是时候初始化整套系统,并看它真正运转起来了。
在本节中,我们将运行这台已经完整组装好的引擎。我们不只是测试“它能不能跑”,更是在展示:这些架构升级确实带来了一个透明的、具备动态多步骤推理能力的系统。
可视化 trace
我们的 engine.py 模块现在已经完整了。不过,在 notebook 环境中我们还需要再加上一个最终工具。日志确实为审计提供了详细记录,但若想立刻对引擎行为形成直观理解,一份快速、可视化的摘要会非常有价值。
为此,我们会保持 engine.py 库本身尽量干净,同时在 notebook 中定义一个专门的展示函数,叫做 execute_and_display。它会扮演我们的“机房控制台”,负责完整地处理整个过程:调用引擎、接收最终结果与详细 trace 对象,并把两者格式化为清晰、面向人的输出展示。这种方式把核心引擎逻辑与面向用户的输出清晰地区分开来。
它会在“最终预生产 notebook”一节中整合进去。现在,我们先来运行一次标准执行。
标准工作流执行
第一项测试使用一个简单、熟悉的目标,与第 4 章中的例子类似。我们要求引擎写一段悬疑场景,这应该自然触发经典的 Librarian → Researcher → Writer 工作流。
这项测试的目标,是确认重构后的 Context Engine 能否从头到尾正确组装并执行一条基础的多智能体计划。我们先定义目标,运行引擎;如果执行成功,再用新的 display_trace() 方法来可视化系统究竟是如何达成结果的:
#Example 1
# Define the high-level goal
goal = "Write a short, suspenseful scene for a children's story about the Apollo 11 moon landing, highlighting the danger."
# Call the execution function from the cell above
execute_and_display(goal, config, client, pc)
输出会先展示故事本身:
--- FINAL OUTPUT ---
July 20, 1969. The cabin hums. The Moon fills the window. Gray. Still. Strange.
I’m in Eagle with Buzz. First try. No second chances.
The small computer chirps. 1202. Then 1201. My chest tightens. We ask. Voices crackle in my headset. Houston comes back fast. “Go.” We keep dropping.
Shadows stretch across the ground. Big. Long…
然后按我们的要求展示 trace:
--- TECHNICAL TRACE (for the tech reader) ---
Trace Status: Success
Total Duration: 148.33 seconds
Execution Steps:
[ { 'agent': 'Librarian',
'output': { 'blueprint_json': '{"scene_goal": "Increase tension and create '…
复杂工作流执行
第二项测试会把 Planner 推出简单线性工作流的舒适区。这次,我们给它一个更复杂的目标:先写一段关于 Apollo 11 登月的事实摘要;然后,再用欧内斯特·海明威那种极简而有冲击力的风格,把这段内容重写一遍。
这个任务无法用通常的 Librarian → Researcher → Writer 顺序来解决。一个真正智能的 Planner 必须识别出,这是一个两阶段流程:
- 阶段 1 —— 生成:收集事实,写出初版描述;
- 阶段 2 —— 重写:检索海明威风格蓝图,并把它应用到第 1 阶段生成的内容上。
这个场景迫使 Planner 将两个 Writer 串联起来:第一个 Writer 的输出,要成为第二个 Writer 的输入。这是对上下文链式传递的有力展示,也证明这台引擎并不是在机械执行固定脚本,而是在动态地推理:为了达成目标,究竟需要哪些步骤。你可以在 notebook 中这样修改目标:
goal_2 = "First, write a factual summary of the Apollo 11 landing. Then, rewrite that summary in the minimalist, impactful style of Ernest Hemingway."
…
输出会给出一段非常锐利的“海明威记者风”回应:
Here’s the quick-and-casual lowdown on Apollo 11
- First crewed Moon landing, round trip in about 8 days. Launched on a Saturn V from Kennedy Space Center (LC-39A).
- Dates: Launch July 16, 1969; landing July 20 (20:17:40 UTC); first step July 21 (02:56:15 UTC); splashdown July 24.
- Crew: Neil Armstrong (Commander), Buzz Aldrin (Lunar Modul
然后同样会输出 trace:
--- TECHNICAL TRACE (for the tech reader) ---
Trace Status: Success
Total Duration: 151.31 seconds
Execution Steps:
[ { 'agent': 'Researcher',
'output': { 'facts': 'Note: The provided “Sources” are unrelated to Apollo '…
既然我们已经跑通了这两种工作流,现在就该深入表层之下,看看引擎究竟是如何一步步完成推理的。最好的方式,就是查看执行日志(execution log)。
拆解引擎的思维过程
执行日志是 Context Engine 的实时运行转录。它揭示了系统是如何把一个高层目标转化为一系列结构化、具体化动作的,并以非常细致的方式展示了:规划、执行和上下文链式传递是如何在引擎内部展开的。
启动与规划
=== [Context Engine] Starting New Task ===
Goal: Write a short, suspenseful scene for a children's story about the Apollo 11 moon landing, highlighting the danger.
[Engine: Planner] Analyzing goal and generating execution plan...
[Engine: Planner] Plan generated successfully.
这一开始的日志块说明:引擎已经接收到目标。Planner 会立即启动,对请求进行分析,查询 Agent Registry 以了解有哪些可用工具,并制定一份三步 JSON 战略计划来实现这个目标。这就是引擎的“思考阶段”。
执行步骤 1 —— Librarian
[Engine: Executor] Starting Step 1: Librarian
[Librarian] Activated. Analyzing intent...
[Librarian] Found blueprint 'blueprint_suspense_narrative' (Score: 0.66)
[Engine: Executor] Step 1 completed.
Executor 现在开始按照计划行动。它激活 Librarian 智能体,并传入 intent_query:"suspenseful children's story"。Librarian 接着把这个查询转换成向量,并在 Pinecone 的 ContextLibrary 命名空间中执行检索。它找到了一个名为 'blueprint_suspense_narrative' 的匹配蓝图,其中包含如何以悬疑语气写作的指令(例如使用短句、聚焦感官细节、逐步提升张力)。
执行步骤 2 —— Researcher
[Engine: Executor] Starting Step 2: Researcher
[Researcher] Activated. Investigating topic...
[Researcher] Found 2 relevant chunks. Synthesizing...
[Engine: Executor] Step 2 completed.
接下来,Executor 激活 Researcher 智能体,传入 topic_query:"Apollo 11 moon landing danger"。Researcher 在 KnowledgeStore 命名空间中进行检索,取回两段与 Apollo 11 任务相关的事实文本。随后,它借助 LLM 将这些事实综合成一段简明摘要,并将其作为输出。
执行步骤 3 —— Writer
[Engine: Executor] Starting Step 3: Writer
[Engine: Executor] Resolved dependency STEP_1_OUTPUT.
[Engine: Executor] Resolved dependency STEP_2_OUTPUT.
[Writer] Activated. Applying blueprint to source material...
[Engine: Executor] Step 3 completed.
这是最后一步,也是最关键的一步。Executor 首先执行上下文链式传递:它把 $$STEP_1_OUTPUT$$ 占位符解析成 Librarian 返回的悬疑蓝图,再把 $$STEP_2_OUTPUT$$ 解析成 Researcher 返回的事实摘要。随后,Writer 被激活,并同时接收到风格指令和事实内容作为输入。它将两者融合,生成最终文本。
收尾
随着最后一步完成,引擎任务顺利结束:
=== [Context Engine] Task Complete ===
最终输出,是这条三智能体工作流直接协同的产物。它既融合了请求的风格,又扎根于已验证的事实,实现了完美的协调:
******** FINAL OUTPUT 1 **********
July 20, 1969. Apollo 11. NASA’s Apollo program. The Space Race hums in the background like a distant storm.
Three of us. Neil Armstrong. Buzz Aldrin. Michael Collins.
The Moon fills the window. Black sky. Hard light. Long shadows.
...
The hum rises, falls. The Eagle steadies. The scene tightens to a pin.
Then—stillness.
...
A boot hovers. I watch the shadow first. It kisses the surface before I do.
Then contact. A first step. On the Moon.
...
The moment stretches thin, like a note, and does not break.
让我们来分析一下这个输出:
- Librarian 的影响(风格) :输出与
'blueprint_suspense_narrative'完全匹配。句子短促、直白、有冲击力(例如 “The cabin is small.” “Fuel is low.”)。它聚焦于感官细节(例如 “A soft hiss in my ears,” “A tremor underfoot”),并使用富有张力的语言(例如 “The scene tightens to a pin,” “The moment stretches thin...and does not break”)。这正是蓝图中风格指导的直接体现。 - Researcher 的影响(事实) :故事扎根于 Researcher 检索到的事实信息。它准确提到了日期(1969 年 7 月 20 日)、任务名称(Apollo 11)、机组成员(Armstrong、Aldrin、Collins)以及登月舱名称(Eagle)。
这个输出清楚地表明,强化后的 Context Engine 正在按预期工作。它成功地规划出动态工作流,编排多个智能体,并通过上下文链式传递生成了一个连贯的结果。我们已经正式从“聪明的原型”迈向了“稳健、抗压、透明的系统”。下一步,就是让它在使用层面也变得足够顺滑。
引擎模块化
当 Context Engine 的每一个组件都已经完成强化之后,我们就可以迈出这段从原型走向预生产旅程中的最后一步,也是最关键的一步:模块化重构。在这个阶段,我们将通过把核心逻辑迁移到独立的 Python 库文件中,把原本单一、庞大的 notebook,改造成一个干净且可扩展的应用程序。
这个过程绝不仅仅是“复制粘贴代码”那么简单。它更像是一段实际调试旅程,能真实揭示 Python 模块之间是如何通信、如何管理依赖、以及如何交换数据的。我们会从一个干净的 notebook——Context_Engine_Pre_Production.ipynb——开始,把它作为我们的“控制台”,然后把强化后的代码组织进一个专门的 commons/ 库目录中。
我们的目标,是把 Context_Engine_MAS_MCP.ipynb 中第 3、4、5、6 节的代码分别拆分到各自的 .py 文件中:helpers.py、agents.py、registry.py 和 engine.py,并统一放入新的 commons/ 目录下。这个重构过程,标志着我们的 Context Engine 已经完成从原型到模块化、可扩展应用程序的最终转化。
本地导入
我们的第一个挑战,是运行环境问题。对于结构化项目而言,像 from commons import helpers 这样的导入语句,在扁平的 Colab 目录里会失败,因为所有文件都下载在同一层目录下。我们必须调整导入方式,让它适应这种扁平结构,把每个文件都当成顶层模块来处理。
在 Colab 中,需要改成下面这样的代码:
# This works in a flat directory by importing each file directly.
import helpers
import agents
from registry import AGENT_TOOLKIT
from engine import context_engine
这样,notebook 就能看见这些新的库文件了。但此时,库文件彼此之间还看不见对方。
模块独立性
一运行新的导入单元,我们就立刻学到一个关键经验:每一个 Python 文件都是一座孤岛。一个模块对外部世界一无所知,包括其他模块——除非你显式告诉它。
问题 1:缺失的 agents
第一个报错出现在导入 registry.py 时:
NameError: name 'agent_context_librarian' is not defined
registry.py 的职责,就是知道所有智能体的存在。但这些智能体函数现在定义在另一个文件——agents.py——中。由于 registry 本身是一个独立模块,它根本不知道 agents.py 的存在。
解决方法是:我们必须显式连接这些模块。我们需要编辑 registry.py,导入 agents 模块,然后在引用每个函数时加上 agents. 前缀,让模块知道函数在哪里定义。
修改后的 registry.py 如下:
# Tell this file about the agents module
import agents
import logging
class AgentRegistry:
def __init__(self):
self.registry = {
# Specify where to find each function
"Librarian": agents.agent_context_librarian,
"Researcher": agents.agent_researcher,
"Writer": agents.agent_writer,
}
# ... rest of the class
随着这个问题的解决,我们会继续在其他文件里逐步识别并导入它们的依赖。比如 engine.py 需要导入 helpers 和 registry,而 agents.py 需要导入 helpers 中的函数。这种按图索骥式的调试过程,恰恰完美体现了如何构建一个真正模块化的系统:每个组件都必须显式声明自己需要什么。
问题 2:数据结构不匹配
所有导入错误解决之后,引擎终于能跑起来了,但它在最后一步崩溃了。这次的问题更隐蔽:各个智能体之间传递的是结构化字典(例如 {'blueprint_json': '...'}),而 Writer 智能体却仍然期望拿到原始字符串作为源材料。结果,Writer 最终输出的不是故事,而是一个错误。
修复办法,是让 Writer 更“聪明”一些。我们修改了它,让它在读取输入时先检查:blueprint 和 facts 是否是字典。如果是,就从正确的 key 里提取出字符串值,再继续处理。这样一来,无论内部数据是以何种结构传递,Writer 最终都能拿到它真正需要的原始文本。
agents.py 中 agent_writer 的修改代码如下:
# --- FIX: Unpack the structured inputs from previous steps ---
blueprint_data = mcp_message['content'].get('blueprint')
facts_data = mcp_message['content'].get('facts')
previous_content_data = mcp_message['content'].get('previous_content')
# Extract the actual strings, handling both dict and raw string inputs
blueprint_json_string = blueprint_data.get('blueprint_json')
if isinstance(blueprint_data, dict)
else blueprint_data
facts = facts_data.get('facts')
if isinstance(facts_data, dict)
else facts_data
previous_content = previous_content_data # Assuming this is already a string if provided
有了这最后一个修复,我们的重构之旅也就完成了。我们已经成功地把一条线性 notebook,转化为了一个专业、模块化的应用程序。现在,我们终于可以构建最终版 notebook 了。
最终预生产 notebook
我们最终的 notebook——Context_Engine_Pre_Production.ipynb——现在已经非常干净,并且承担着这台强大引擎的高层“控制台”角色。它主要只有两个部分。
集中化执行函数(引擎室)
第一部分定义了一个单独的函数,execute_and_display(),它把运行引擎与展示结果的全部逻辑都封装在一起。它接收用户目标、一个配置字典,以及已经初始化好的客户端对象;然后打印两类内容:一类是给普通用户看的精美最终输出,另一类是给开发者看的详细技术 trace。
# === ENGINE ROOM: The Main Execution Function ===
# This function contains all the logic to run the engine.
# We define it here so our final cell can be very simple.
import logging
import pprint
from IPython.display import display, Markdown
def execute_and_display(goal, config, client, pc):
"""
Runs the context engine with a given goal and configuration,
then displays the final output and the technical trace.
"""
logging.info(f"******** Starting Engine for Goal: '{goal}' **********\n")
# 1. Run the Context Engine using the provided configuration
result, trace = context_engine(
goal,
client=client,
pc=pc,
**config # Unpack the config dictionary into keyword arguments
)
# 2. Display the Final Result for the main reader
print("--- FINAL OUTPUT ---")
if result:
display(Markdown(result))
else:
print(f"The engine failed to produce a result. Status: {trace.status}")
# 3. Display the Technical Trace for the developer/technical reader
print("\n\n--- TECHNICAL TRACE (for the tech reader) ---")
if trace:
print(f"Trace Status: {trace.status}")
print(f"Total Duration: {trace.duration:.2f} seconds")
print("Execution Steps:")
# Use pprint for a clean, readable dictionary output
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(trace.steps)
至此,我们已经拥有了一个集中化的执行流程。接下来再看看用户如何与系统交互。
用户交互(控制台)
最终那个单元格,是用户唯一需要直接交互的地方。用户只需在这里定义高层目标,并填写一个 config 字典,其中保存本次运行所需的全部技术参数(例如模型名称和 Pinecone 命名空间)。这种把逻辑放在“引擎室”、把用户输入放在“控制台”的关注点分离方式,正是一套设计良好的应用程序应有的样子。
配置会写在用户目标上方的一个单元格中:
# 1. Define all configuration variables for this run in a dictionary
config = {
"index_name": 'genai-mas-mcp-ch3',
"generation_model": "gpt-5",
"embedding_model": "text-embedding-3-small",
"namespace_context": 'ContextLibrary',
"namespace_knowledge": 'KnowledgeStore'
}
而真正的控制台现在已经极度简化:
#Example 1
# Define the high-level goal
goal = "[YOUR GOAL]"
# Call the execution function from the cell above
execute_and_display(goal, config, client, pc)
系统会生成与“运行强化后的引擎”一节中相同结构的输出。到这里,我们已经构建出一套扎实的预生产级 Context Engine。接下来,让我们总结一下整个过程。
总结
在本章中,我们把 Context Engine 从一个功能性原型,提升成了一套预生产系统。通过对其架构进行重构和重建,我们贯彻了专业软件工程原则,使这台引擎变得稳健、透明且可靠,并由此给出了一个构建高韧性多智能体系统的实践蓝图。
影响最大的改动,是引入了依赖注入。它消除了对全局变量的依赖,使每个组件都变得自包含,并对自身依赖显式可知。这种解耦设计极大提升了引擎的灵活性、可测试性与可维护性。
我们也对各个子系统进行了强化——从辅助函数到智能体——通过加入结构化日志与精确错误处理,打造了一套完全可审计、可容错的工作流。最后,我们展示了如何把一个单体 notebook 重构为模块化应用程序,并说明了:独立的库文件与清晰的模块间通信,是如何共同构成一个可扩展项目结构的。
最终成果,是一套经过专业架构设计的 Context Engine:它在用户可见的“控制台”与模块化“核心引擎”之间建立了清晰分层。现在,它已经能够作为一组专用、独立组件的集合,通过一条可预测、可透明追踪的工作流被统一编排起来。
下一章中,我们将开始把这台 context engine 应用于真实世界中的实现问题与实际用例。
问题
- 本章的主要重点,是从零开始构建 Context Engine 的初始原型吗?(是或否)
- 在规划阶段,
planner()函数会立即调用专用智能体去执行实际工作吗?(是或否) resolve_dependencies()函数的主要作用,是通过把$$STEP_1_OUTPUT$$这类占位符替换成真实数据来实现上下文链式传递吗?(是或否)- 文中是否将 Agent Registry 描述为管理整个流程的系统“大脑”?(是或否)
- 在重构过程中创建
utils.py文件,是为了处理引擎的高层编排与规划逻辑吗?(是或否) - 在原始第 4 章原型中,主要的日志记录方式是 Python 内置的
logging模块吗?(是或否) - 在重构 Librarian 和 Researcher 智能体时,是否发现了一个关键缺陷:它们返回的是原始字符串,而不是结构化字典?(是或否)
- 当代码从 notebook 迁移到独立的
.py文件时,registry.py是否因为找不到智能体函数而崩溃?(是或否) - 复杂工作流执行测试(用海明威风格重写)是否证明了该引擎始终遵循固定的 Librarian → Researcher → Writer 顺序?(是或否)
参考文献
Qian, C., Wu, Y., Zhang, J., Wu, Y., Ahmed, J., Liu, B., & Lo, D. (2024). LLM-Based Multi-Agent Systems for Software Engineering: Literature Review, Vision and the Road Ahead. arXiv preprint arXiv:2404.04834.
Zhang, Y., Li, R., Liang, P., Sun, W., & Liu, Y. (2025). Knowledge-Based Multi-Agent Framework for Automated Software Architecture Design. arXiv preprint arXiv:2503.20536.
Zhang, W., Wang, Z., Li, Y., Chen, Z., Zhang, Z., Liu, S., ... & Liu, B. (2025). AgentOrchestra: A Hierarchical Multi-Agent Framework for General-Purpose Task Solving. arXiv preprint arXiv:2506.12508.
Cito, J., Kassab, M., & Parnin, C. (2021). Cataloging Dependency Injection Anti-Patterns in Software Systems. arXiv preprint arXiv:2109.04256.
延伸阅读
Wooldridge, M. (2009). An Introduction to Multiagent Systems (2nd ed.). John Wiley & Sons.