多智能体系统上下文工程-超越提示词-构建上下文与推理的透明架构引擎-二-
多智能体系统上下文工程:超越提示词,构建上下文与推理的透明架构引擎(二)
原文:Context Engineering for Multi-Agent Systems
译者:飞龙
构建上下文感知多智能体系统
"metadata": {
"text": batch_texts[j]
}
# 插入批量数据
index.upsert(vectors=batch
当此代码运行时,它将回溯上下文库和知识库的上传情况。tqdm 进度条会实时显示每个嵌入步骤。在这个示例中,上下文库确认已成功上传了三个蓝图。由于知识库较小,被分为两个块(chunks),可以放入一个 100 条数据的批次中。随后,上传确认两个块都已成功存储:
处理并上传 Context Library 到命名: ContextLibrary
[tqdm output for 3/3 iterations]
成功上传了 3 个上下文向量。
处理并上传 Knowledge Base 到命名: KnowledgeStore
创建了 2 个知识块。
[tqdm output for 1/1 iterations]
成功上传了 2 个知识向量。
在上下文库和知识库填充完成后,我们的向量存储现在已准备就绪。构建上下文感知系统的组件已就位。
构建上下文感知系统
现在我们的数据已经准备好并存储在 Pinecone 中,是时候将一切整合在一起了。在本节中,我们将实现图 3.3 所示的上下文感知系统。步骤遵循 Context_Aware_MAS.ipynb 笔记本,它构建了我们架构的运行时执行阶段。我们将首先定义处理核心任务的专家智能体,然后查看协调器(Orchestrator)是如何协调它们的。
图 3.3: 上文感知 MAS 流程
流程图展示了高层用户目标如何转换为结构化工作流。这提醒我们,生成式 AI 并不是“就能用”。精细的工程和上下文管理是使系统可靠的关键。
在图中,协调器(蓝色框)充当中央协调,将任务分配给专家智能体(绿色框):管理员(Librarian)、研究员(Researcher)和作家(Writer)。这些智能体与外部服务(橙色框)交互,例如 Pinecone(用于知识和蓝图的检索)和 LLM(用于嵌入、分析和生成)。
在大局已定后,让我们转向细节,从智能体本身开始。
定义智能体
MAS 依赖于三个专门的智能体。每个智能体执行定义的角色:
-
上下文管理员 (Context Librarian):从上下文库中检索程序化指令
-
研究员 (Researcher):从知识库中提取并合成事实数据
-
作家 (Writer):将程序化指令与事实数据结合以生成最终输出
所有智能体均使用 MCP 进行通信。让我们从管理员智能体开始。
上下文管理员智能体
管理员智能体负责从向量存储中检索上下文语义蓝图。我们展示了 RAG 的高级用法,即检索程序化指令。我们不是在提取事实,而是在提取如何撰写:
def agent_context_librarian(mcp_message):
"""
从上下文库中检索合适的语义蓝图。
"""
print("[Librarian] 已激活。正在分析意图...")
requested_intent = mcp_message['content']['intent_query']
results = query_pinecone(requested_intent, NAMESPACE_CONTEXT, top_k=1)
if results:
match = results[0]
print(f"[Librarian] 找到蓝图 {match['id']} (得分: {match['score']:.2f})")
blueprint_json = match['metadata']['blueprint_json']
content = {"blueprint": blueprint_json}
else:
print("[Librarian] 未找到特定蓝图。返回默认值。")
content = {"blueprint": json.dumps({"instruction": "生成中性内容。"})}
return create_mcp_message("Librarian", content)
这里发生的情况是管理员使用用户的意图(intent_query)语义搜索 ContextLibrary 命名空间。当找到匹配的蓝图时,提取 JSON 指令并封装新的 MCP 消息中。这允许作家智能体遵循动态程序化指南。我们现在需要构建研究员智能体。
正如代码所示,如果管理员智能体没有找到匹配的上下文蓝图,它将返回一组默认指令。系统设计回退机制,以处理用户意图不匹配任何可用语义蓝图的情况。管理员智能体并没有报错,而是创建一个带有简单指令的默认蓝图:以中性方式生成内容。这确保了作家智能体仍收到程序化指导,允许系统产生基础的、无风格的响应而不是完全停止进程。
研究员智能体
研究员智能体专注于提取相关的事实信息。该智能体查询知识库获取最相关的数据块,并将其合成为简洁的摘要,供作家可以直接使用:
def agent_researcher(mcp_message):
"""
从知识库中检索并合成事实信息。
"""
print(f"\n[Researcher] 已激活。正在调查主题...")
topic = mcp_message['content']['topic_query']
results = query_pinecone(topic, NAMESPACE_KNOWLEDGE, top_k=3)
if not results:
print(f"[Researcher] 未找到相关信息。")
return create_mcp_message("Researcher", {"facts": "未找到数据。"})
source_texts = [match['text'] for match in results]
system_prompt = f"""你是一个专家研究 AI。综合所提供的来源。
{n'.join(source_texts)}"""
findings = call_llm(system_prompt)
return create_mcp_message("Researcher", {"facts": findings})
作家智能体
作家智能体负责将程序化指令与事实数据结合以生成最终输出。它根据 LLM 接收到的蓝图和事实数据来生成最终内容。
def agent_writer(mcp_message):
"""
将程序化指令与事实数据结合以生成最终输出。
"""
print(f"\n[Writer] 已激活。正在调查主题...")
blueprint_json = mcp_message['content']['blueprint_json']
facts = mcp_message['content']['facts']
system_prompt = f"""你是一个专家研究 AI。根据以下事实生成输出:
{blueprint_json}"""
output = call_llm(system_prompt)
return create_mcp_message("Writer", {"output": output})
构建协调器 (Orchestrator)
协调器是上下文感知系统的总枢。它首先通过 LLM 拆解用户目标的意图和主题。
编排器(Orchestrator)
def orchestrator(high_level_goal):
"""
管理上下文感知 MAS 的流程。
"""
print(f"== [Orchestrator] 开始新任务 ===")
analysis_system_prompt = """你是一个目标分析师。分析用户的高层目标并提取以下两个组件:
1. 'intent_query': 描述所需风格、语气或格式的短语。
2. 'topic_query': 总结所需事实内容的短语。
仅返回包含这两个键的 JSON 对象。"""
analysis_result = call_llm(analysis_system_prompt, high_level_goal, json_mode=True)
try:
analysis = json.loads(analysis_result)
intent_query = analysis['intent_query']
topic_query = analysis['topic_query']
except Exception as e:
print(f"解析目标失败: {e}")
return
# 步骤 1: 查询管理员
print(f"\n[Orchestrator] 查询管理员...")
librarian_message = create_mcp_message("Librarian", {"intent_query": intent_query})
librarian_response = agent_context_librarian(librarian_message)
blueprint_json = librarian_response.content['blueprint_json']
# 步骤 2: 查询研究员
print(f"\n[Orchestrator] 查询研究员...")
research_message = create_mcp_message("Researcher", {"topic_query": topic_query})
research_response = agent_researcher(research_message)
facts = research_response.content['facts']
# 步骤 3: 查询作家
print(f"\n[Orchestrator] 调用作家...")
writer_message = create_mcp_message("Writer", {
"blueprint_json": blueprint_json,
"facts": facts
})
writer_response = agent_writer(writer_message)
return writer_response.content['output']
topic_query = analysis['topic_query']
except (json.JSONDecodeError, KeyError):
return
编排器(Orchestrator)亲自调用 LLM 将高级目标解析为结构化的 JSON。如果解析失败,函数将返回自身——这防止了系统产生级错误。
注意: 此
try...except块有效地充当了回退机制:如果 LLM 生成了无效或不完整的 JSON,编排器将在将损坏的数据传递下游后优雅地停止。在生产环境中,你可以通过结构化提示词或 Schema 验证进一步固化这一步骤,但对于这个原型来说,这种轻量级的方法保持了系统的弹性且易于理解。
代理协作
一旦目标被分解,编排器就会协调代理之间的信息流。它向管理员(Librarian)发送 intent_query,向研究员(Researcher)发送 topic_query,然后将两个结果都交给写作者(Writer)。
编排器查询管理员获取上下文蓝图:
# Step 1: Get the Context Blueprint (Procedural RAG)
mcp_to_librarian = create_mcp_message(
sender="Orchestrator",
content={"intent_query": intent_query}
)
display_mcp(mcp_to_librarian, "Orchestrator -> Librarian")
mcp_from_librarian = agent_content_librarian(mcp_to_librarian)
display_mcp(mcp_from_librarian, "Librarian -> Orchestrator")
context_blueprint = mcp_from_librarian['content'].get('blueprint')
if not context_blueprint: return
编排器从研究员处检索事实性知识:
# Step 2: Get the Factual Knowledge (Factual RAG)
mcp_to_researcher = create_mcp_message(
sender="Orchestrator",
content={"topic_query": topic_query})
第 3 章
# display_mcp(mcp_to_researcher, "Orchestrator -> Researcher")
mcp_from_researcher = agent_researcher(mcp_to_researcher)
display_mcp(mcp_from_researcher, "Researcher -> Orchestrator")
research_findings = mcp_from_researcher['content'].get('facts')
if not research_findings: return
最后,编排器让写作者生成输出:
# Step 3: Generate the Final outputs for the Writer Agent
# Combine the outputs for the Writer Agent
writer_task = {
"blueprint": context_blueprint,
"facts": research_findings
}
mcp_to_writer = create_mcp_message(
sender="Orchestrator",
content=writer_task
)
# display_mcp(mcp_to_writer, "Orchestrator -> Writer")
mcp_from_writer = agent_researcher(mcp_to_writer)
display_mcp(mcp_from_researcher, "Writer -> Orchestrator")
final_result = mcp_from_writer['content'].get('output')
print("\n=== [Orchestrator] Task Complete ====")
final_result
这种模式使职责变得非常清晰:管理员检索过程性指导,研究员收集事实,而写作者通过将两者结合来生成最终文本。此外,这种架构非常稳健,因为它是模块化的并且为了扩展而构建的。你可以获得可靠的、感知上下文的内容,因为每个代理都只负责一项工作,并且系统遵循你给出的过程化指令。编排器像一个优秀的项目经理——它引导工作流,但不干涉细节,让专家们专注于自己的工作。
真正的规则改变者是将"做什么"与"如何做"分离。我们可以在不触碰风格规则的情况下更新知识库,反之亦然,这使得迭代变得极其快速,这是一个巨大的胜利。
由于语义蓝图是动态的,你可以为任何任务切换风格、语气和结构。这种灵活性使得该设计非常强大。最终,这不仅仅是一个学术练习,更是构建能够处理复杂上下文任务的智能代理系统(MAS)的实用蓝图。
总结
本章从改进第二章的代理开始。我们架构的核心是双重 RAG 方法。我们在单个 Pinecone 索引中创建了两个独立的数据库。知识库存储研究员代理的事实信息。上下文库存储管理员代理的过程性语义蓝图。这种分离允许系统独立地管理其所知和其行为。
我们通过两个部分实现了该系统。第一个笔记本准备并上传数据到两个 Pinecone 命名空间。第二个笔记本定义了代理工作流。我们展示了编排器如何分析用户目标。它将事实发现交给研究员,将风格检索交给管理员。写作者结合事实和风格蓝图生成约束输出。我们的系统现在可以动态控制其生成过程。我们准备好进入下一个阶段,利用所构建的内容设计上下文引擎。
问题
-
系统是否对一切都使用一种 RAG?(是或否)
-
上下文库是否仅存储关于太空探索的事实?(是或否)
-
研究员和管理员是否在同一个地方查找信息?
-
编排器是否生成最终文本?
-
编排器是否分析用户目标?
-
系统是否使用了两种不同的 RAG 方法?
-
研究员是否存储上下文蓝图?
-
管理员是否存储过程性语义蓝图?
-
管理员是否用于风格检索?
-
写作者是否生成最终输出?
构建上下文引擎(Context Engine)
人工智能的演进已从僵化的、基于规则的系统转向灵活的生成式模型。将早期人工智能(通常被称为符号 AI)想象成精心手工编写的规则书。它们在特定领域内令人印象深刻,但在面对模糊性和任何脚本之外的内容时却非常脆弱。现代大语言模型(LLMs)反转了这一模式。它们展现出广泛的语言理解能力和通用推理能力,但缺乏特殊性、实时知识,且无法可靠地依赖外部系统。仅靠 LLMs 很难提供持续的业务价值。这种差距导致了你在前几章已经看到的两个转变:RAG(检索增强生成)通过外部来源的事实上下文为 LLMs 提供依据,减少幻觉并使响应具备场景感知;其次,AI 代理为 LLMs 提供了工具和程度的自主性,使其能够执行多步任务。这些想法共同推动模型更接近实用的工作。
然而,这种快速演进带来了一个新的架构挑战。早期的代理系统硬编码了代理之间的交接,这在演示中看起来还可以,但在生产环境中会迅速变成一团乱。如果你有多个代理协同工作,你一定体会过这种痛苦。随着团队添加专门代理,线性的组合无法推理其自身能力,也无法适应新问题。因此,问题变成了:我们如何从代理集合转向自适应、自主的系统?这就是上下文引擎大显用场。
上下文引擎是一个元系统,一个智能控制器,它围绕高级目标组织专门代理。它不仅仅是一个单一函数;它是一个结构化工作流,将模糊的意图转化为落地的、感知上下文的输出。虽然"上下文引擎"标签特定于此框架,但这种模式在高级生成式 AI 平台上被广泛使用。ChatGPT 或 Gemini 等系统并非孤立的模型,而是带有控制器的应用程序,用于管理会话、调度工具并维护上下文。从概念上讲,这些控制器的角色与上下文引擎相同。
架构的独特之处在于规划与执行的显式分离,这灵感来自代理推理的前沿研究。上下文引擎并非简单的循环,而是采用了一个两阶段的过程:
-
首先,规划器(
Planner) 作为战略核心,对用户目标进行推理,并根据代理注册表(Agent Registry) 作为可用能力的"工具箱",选择最佳的代理或函数。因此,我们将在本章中构建这些新的规划器和代理注册表工具。 -
然后,规划器使用
LLM生成一个针对特定任务定制的动态、分步骤的执行计划。一旦计划就绪,它将移交给执行器(Executor),执行器是运营经理,按正确的顺序调用专门代理。
注意:如第 3 章介绍的,我们将利用带有静态知识数据和动态指令的双重 RAG 来增强上下文引擎的灵活性。
在本节中,我们将设计上下文引擎。我们将从架构的可视化流程遍历开始,然后解析它所依赖的组件。概览请参见图 4.1。
架构概述
上下文引擎是一个动态的多阶段工作流,如图所示。每种颜色代表一个不同的功能层,它们协作将高级目标转化为完全生成的输出。
我们从最外层开始,流程从用户开始并结束于用户。橙色组件代表这两个触点:用户高级目标和最终输出。目标是一个宽泛的指令,例如"写一个悬疑故事"。这是一个不带任何预定义结构的简单请求。另一方面,最终输出是满足该目标的完整、精炼的结果。
在一切的中心是上下文引擎本身,用蓝色表示。它是协调器,是不直接执行任务的大脑,负责通过战略智能管理整个工作流。它内部包含三个关键模块:
-
规划器(
Planner): 战略核心,接收用户目标并设计分步骤计划。 -
执行器(
Executor): 运营经理,执行计划,调用专门代理并管理它们之间的数据流。 -
追踪器(
Tracer): 透明的记录器,记录每个操作以供调试和提供洞察。
该工作流的第一阶段是战略规划,此时规划器与黄色和红色组件协作。它首先咨询黄色高亮的代理注册表,这是一份列出所有可用代理及其能力的工具箱。通过这些信息,它连接到外部的红色高亮服务——即 LLM(例如 GPT-5)。规划器将用户目标和工具箱同时发送给 LLM,然后 LLM 返回一个结构化的 JSON 计划。该计划在图中以虚线箭头表示,流回引擎并移交给执行器。
第二阶段是执行,执行器协调绿色的专门代理和外部的红色服务。它按照计划逐步执行,调用了以下内容:
-
管理员(
The Librarian): 处理过程性 RAG 以获取风格蓝图。 -
研究员(
The Researcher): 执行事实 RAG 以收集并合成相关信息。 -
作者(
The Writer): 结合风格和事实产生最终内容。
这些代理都会与外部服务交互。管理员和研究员查询向量数据库(Vector DB),而研究员和作者可能会调用 LLM 来精炼事实或生成文本。
一个关键创新是上下文链链(在第 1 章引入),由循环的绿色箭头表示。这种机制确保了有状态的工作流,其中一个代理的输出(例如管理员的蓝图)成为下一个代理(作者)的输入。
功能深度解析
有了这个概述,我们现在准备好探索让上下文引擎运行起来的功能。让我们看看核心函数:
-
Context Engine(): 主协调器,运行规划器和执行器。 -
Planner(): 驱动引擎,使用AgentRegistry.capabilities_description()和call_llm_robust()生成计划。 -
Executor: 迭代计划,使用resolve_dependencies()组装当前代理的上下文。 -
Agent Registry: 使用get_handler()获取正确的代理。 -
Tracer: 实现每一步的日志。
专门代理(绿色):
-
Librarian(): 获取风格蓝图。 -
Researcher(): 合成事实。 -
Writer(): 生成最终内容。
系统组件(蓝色和黄色):
-
Context Engine: 协调函数。 -
Agent Registry: 代理工具箱列表。
工作流遵循这些箭头:
-
用户请求 →
context_engine()。 -
在引擎内部 → 首先调用
planner()。 -
执行器迭代计划。
-
执行器使用
get_handler()获取正确的代理。 -
代理使用:
-
管理员和研究员 →
query_db()(使用embedding()) -
研究员和作者 →
call_llm_robust()
-
每个代理通过 create_message() 返回结果,执行器将其存储。循环持续直到计划完成并返回最终输出。
组装系统
我们花时间了上下文引擎的结构以及每个组件对整体工作流的贡献。在本节中,我们从理解转向构建。我们的重点现在从架构蓝图转向系统本身。Context_Engine.ipynb 将是我们的工作空间,我们将在那里逐步组装引擎,连接齿轮并布线,让我们的图表活起来。
我们将按照逻辑顺序构建引擎,从基础向上。一个强大的协调器的效能取决于它领导的团队,我们的第一个任务是创建专门代理——工具箱,即系统的功能核心。我们的代理已准备就绪,我们将开发它们使其易于发现和管理。
注意:关于延迟:书中构建的上下文引擎以及相关仓库执行的是复杂的多步推理,而不是简单的单次调用。你在
Colab中观察到的延迟是思考时间,引擎正在动态规划并执行一系列 API 调用(例如:规划、然后是 RAG,然后是生成)。这也是Gemini或ChatGPT等高级平台处理复杂请求时需要时间的原因。
专家代理
在我们让上下文引擎(Context Engine)完全运行之前,我们需要构建它的前线专家——即实际执行工作的代理。正如我们已经确定的,多代理系统由三个专门的代理组成:管理员(Librarian)、研究员(Researcher)和作家(Writer)。每个代理都有独特的用途,且所有代理都使用 MCP 进行通信,MCP 充当系统中传输信息的通用语言或标准的"集装箱"。如果没有共享标准,每个代理都需要自定义的集成逻辑,这将系统变得脆弱且难以扩展。MCP 通过确保每条数据(无论是来自管理员的风格蓝图还是来自研究员的事实摘要)都以完全格式封装,解决了这个问题。
这种标准化实现了上下文链链。每个代理接收 MCP 消息作为输入并返回另一个输出,允许执行器(Executor)只需传递数据,而无需担心翻译或格式问题。这种信息的无缝流动使得卓越的多步推理成为可能,也是整个引擎运行的基础。
上下文管理员代理
我们的第一个代理是上下文管理员,实现为agent_context_librarian。它的角色是识别用户意图并从向量库中获取相应的语义蓝图。正如我们之前解释过的,该蓝图为作家提供了关于如何构建和风格化内容的指令。
管理员首先从 MCP 消息中提取intent_query,例如“悬疑叙事蓝图”。这是解释的关键时刻,将创意构想转化为机器可读的内容。它从高层请求中解码出所需的感觉或格式,将“悬疑”等宽泛的概念翻译为针对其知识库的精确、可操作的搜索查询:
def agent_context_librarian(mcp_message):
"""
从上下文库中检索合适的语义蓝图。
"""
print("\n[Librarian] 已激活。正在分析意图...")
requested_intent = mcp_message['content'].get('intent_query')
if not requested_intent:
raise ValueError("管理员要求输入内容中包含 'intent_query'。")
管理员使用query_pinecone()在上下文向量库中执行语义搜索。与其将这个向量库看作事实数据库,不如看作是一个食谱库,每个蓝图都描述了特定类型的写作应该如何结构化或风格化。搜索不仅仅是匹配单词,而是匹配意义,允许系统在措辞不完全匹配时找到最接近的概念契合:
results = query_pinecone(requested_intent, NAMESPACE_CONTEXT, top_k=1)
如果找到了蓝图,它会被封装进 MCP 消息中;否则,管理员仍然返回有效消息,只是带有一个通用指令:
if results:
match = results[0]
print(f"[Librarian] 找到蓝图 {match['id']}: 分数:\n {match['score']:.2f}")
blueprint_json = match['metadata']['blueprint_json']
content = blueprint_json
else:
print(f"[Librarian] 未找到特定蓝图。返回默认值。")
content = json.dumps({"instruction": "中立地生成内容。"})
return create_mcp_message("Librarian", content)
最后一步展示了系统的韧性。在理想情况下,会找到完美匹配且专家制作的蓝图并将其发送给作家,确保高度定制的输出。然而,如果不存在蓝图,系统也不会崩溃。它会优雅地退回到中立指令,确保工作流始终能够完成,尽管结果更加通用。
研究员代理
接下来是研究员(Researcher),系统的调查记者。当管理员处理结构和语调时,研究员负责查找到所有事实。它的工作是查询知识向量库,检索与给定主题最相关的信息,并将其合成成简洁的事实摘要。
让我们看看这个代理如何将一个宽泛的主题转化为聚焦且可验证的简报。该过程从代理从传入的 MCP 消息中接收任务topic_query开始开始。它的第一个动作是对NAMESPACE_KNOWLEDGE向量库进行语义搜索,检索出前-k=3 个最相关的文本块:
def agent_researcher(mcp_message):
"""
从知识库中检索并合成事实信息。
"""
print(f"\n[Researcher] 已激活。正在调查主题...")
topic = mcp_message['content'].get('topic_query')
if not topic:
raise ValueError("研究员要求输入中包含 'topic_query'")
results = query_pinecone(topic, NAMESPACE_KNOWLEDGE, top_k=3)
if not results:
print("[Researcher] 未找到相关信息。")
return create_mcp_message("Researcher", "未找到该主题的数据。")
print(f"[Researcher] 有 {len(results)} 个相关块。正在合成...")
source_texts = [match['metadata']['text'] for match in results]
这是一个关键的设计选择:通过检索多个源,代理收集了比单一结果提供的更全面、更细致的信息集。然而,代理最重要的工作发生在合成阶段。它并不传递检索到的文本,而是使用一个精心设计的提示词(prompt)指示 LLM 充当专家研究 AI。
系统提示词(system prompt)设定了护栏,命令 LLM严格关注事实:
system_prompt = """你是一个专家研究 AI。
将提供的源文本合成简洁的、要点形式的摘要,并与主题相关。严格关注事实。
不要添加外部信息"""
这是减轻幻觉并确保最终摘要基于真实数据的关键步骤。user_prompt 通过将检索到的块组装成清晰的格式完成了这一步:
user_prompt = f"Topic: {topic}\nSources:\n" + "\n\n---\".join(source_texts)
findings = call_llm_robust(system_prompt, user_prompt)
return create_mcp_message("Researcher", findings)
最终合成的结果代表了经过提炼、简洁且与初始主题直接相关的知识。输出以 MCP 消息的形式返回,供下游代理使用。这种检索-合成步骤确保了上下文引擎的最终输出既结构良好(得益于管理员),又在事实可靠。
作家代理
作家是我们流水线上的专家;它是将管理员的蓝图与研究员的结果相结合以生成最终文本的匠。它负责最终的合成,将抽象的风格和原始事实织成流畅、连贯的内容。它还可以处理重写任务,使用之前生成的内容作为输入,这使得引擎具有迭代改进工作的能力。
让我们探索该代理如何组装输入以产生最终输出。代理从 MCP 消息中收集原始材料:来自管理员的蓝图、来自研究员的合成事实,或之前写作步骤的任何previous_content:
def agent_writer(mcp_message):
"""
将事实研究与语义蓝图结合以生成最终输出。
经过关键增强,可以处理原始事实或用于重写任务之前的内容。
"""
print("\n[Writer] 已激活。正在将蓝图应用于素材...")
blueprint_json_string = mcp_message['content'].get('blueprint')
facts = mcp_message['content'].get('facts')
previous_content = mcp_message['content'].get('previous_content')
第 4 章
接下来是一个关键检查:如果没有定义文本编写方式的蓝图,Writer(写入器)将无法执行:
if not blueprint_json_string:
raise ValueError("Writer requires 'blueprint' in the input content")
一旦验证了蓝图,Writer 将通过if/elif/else块确定它正在执行的任务类型:是基于研究创建新内容,还是在重写现有内容?
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")
这种条件逻辑让 Writer 变得非常灵活。它可以在模式(新内容生成或重写)之间切换,而无需改变其核心设计。在大型工作流中,这种灵活性意味着代理(agent)可以被复用于多个精炼阶段。
一旦选择了内容输入,Writer 会构建两个提示词(prompt):一个定义如何写的系统提示词(system prompt),以及一个定义写什么的用户提示词(user prompt):
Writer 的系统提示词包含了动态检索的蓝图
system_prompt = f"""你是一个专家级内容生成 AI。
你的任务是根据提供的 SOURCE MATERIAL 生成内容。
至关重要的是,你**必须**根据下方提供的 SEMANTIC BLUEPRINT 中定义的规则对输出进行结构化、风格化和约束。
--- SEMANTIC BLUEPRINT (JSON) ---
{blueprint_json_string}
--- END SEMANTIC BLUEPRINT --"""
严格遵守蓝图的指令、风格指南和目标。蓝图定义了你**如何**写;而源素材定义了你写**什么**内容。
"""
user_prompt = f"""
--- SOURCE MATERIAL ({source_label}) ---
{source_material}
--- END SOURCE MATERIAL ---
现在请严格按照蓝图生成内容。
"""
这种提示词的这种正是 Writer 真正的智能所在。Writer 的系统提示词提供了创造性框架,以及从蓝图中得出的风格和结构约束。用户提示词则提供了实质内容,即 LLM 必须在其基础上构建的事实或现有内容。
通过将“如何做”与“做什么”分离,Writer 确保了模型的清晰度。LLM 确切知道被要求做什么,以及在什么约束下执行,从而产生风格上一致且有事实依据的输出。
最后,Writer 将提示词发送给 LLM,捕获结果并将其封装成标准的 MCP 消息供下游使用:
final_output = call_llm_robust(system_prompt, user_prompt)
return create_message("Writer", final_output)
此阶段的输出是整个上下文引擎(Context Engine)工作流的结晶。这是一份由证据支持并以精确风格交付的内容内容。
代理注册表 (Agent Registry)
现在我们已经构建了专家代理团队,下一个问题是,谁来跟踪它们呢?Planner(规划器)不能仅仅猜测哪些代理存在或它们具备什么能力。它需要一个可靠的目录,列出所有可用专家、他们的角色以及如何调用他们。为了解决这个问题,我们引入了代理注册表(Agent Registry):一个将代理集合转换为协调团队的中央目录。
AgentRegistry 类负责管理所有代理及其能力,使它们对 Planner 可用。让我们来看看它是如何结构的。
__init__ 方法定义了一个字典self.registry,它是所有可用代理的函数性名单。每个人类可读的名称(例如“Librarian”/管理员)都直接映射到其对应的 Python 函数,例如agent_context_librarian。这种设计使得系统非常灵活。如果以后添加了新代理(例如“Critic”/评论员或“Editor”/编辑),只需在这里注册它们,而无需更改代码库的任何部分:
class AgentRegistry:
def __init__(self):
self.registry = {
"Librarian": agent_context_librarian,
"Researcher": agent_researcher,
"Writer": agent_writer,
}
接下来是让其他组件(如 Executor/执行器)能够根据需要检索这些代理的方法。get_handler() 方法是 Executor 与注册表交互的方式。当 Executor 需要运行计划中的某个特定步骤时,它调用类似get_handler("Librarian")的方法,该方法会返回被调用的函数。这是一个整洁的设计:没有硬编码,没有执行过程中的导入,也没有脆弱的条件判断。只有一个 Executor 精确运行以执行的简单查询:
def get_handler(self, agent_name):
"""获取与代理名称关联的函数。”
handler = self.registry.get(agent_name)
if not handler:
raise ValueError(f"{agent_name}' not found in registry.")
return handler
最后一个也是最重要的方法不是为代码设计的,而是为 LLM 本身设计的。在这里,get_capabilities_description() 返回了关于每个代理目的、输入和输出的结构化、人类可读的描述。这是 Planner 的 LLM 的上下文。当 Planner 调用 LLM 生成计划时,它会包含这段文本,以便模型知道哪些工具可用、每个工具期望什么,以及它们输出什么:
def get_capabilities_description(self):
"""
为 Planner LLM 返回代理的结构化描述。
这对于 Planner 理解如何使用这些代理至关重要。
"""
return """
可用代理及其要求:
1. 代理: Librarian (管理员)
角色: 获取蓝图。
输入:
- "intent": (意图) 目标。
输出: 蓝图 (string)。
2. 代理: Researcher (研究员)
角色: 整合信息。
输入:
- "topic": (string) 主题。
输出: 整合后的的事实 (string)。
3. 代理: Writer (写入器)
角色: 生成或调整内容。
输入:
- "blueprint": (string) 蓝图。
- "facts": (string) 事实。
- "previous_content": (string) 现有内容。
输出: 最终文本 (string)。
"""
该描述的清晰度决定了 Planner 的质量。模糊或不完整的描述会导致计划失败;清晰的描述则赋予推理能力。最后,我们实例化注册表。这一行代码激活了系统:
AGENT_TOOLKIT = AgentRegistry()
有了这个结构,Planner 现在可以对其团队进行智能推理,确切知道它们如何协作。
上下文引擎 (Context Engine)
我们已经了专家并在注册表中记录了他们。下一步是让整个团队共同思考并行动。这就是上下文引擎的工作。
引擎的核心是运行一个简单但强大的循环,模仿人类解决复杂问题的方法:规划、执行、反思。Planner 进行战略思考,将宏大目标拆解为清晰的计划。Executor 投入执行执行该计划,在正确的时间调用正确的代理,并在执行过程中传递上下文。Tracer 扮演着安静但至重要的观察者角色,记录每一步,以便我们之后不仅看到系统做了什么,还能知道为什么。
在接下来的章节中,我们将从 Planner 开始,进入 Executor,然后添加 Tracer。最后,我们在context_engine()中将它们连接起来,并运行一个示例。
Planner
planner 函数是引擎的策略核心。它充当项目经理,将模糊的高层目标转化为精确的、分步骤的、机器可读的 JSON 计划。它的来自于利用 LLM 进行推理的能力。
def planner(goal, capabilities):
"""
分析目标并使用 LLM 生成结构化的执行计划。
"""
print("[Engine: Planner] Analyzing goal and generating execution plan...")
在这里,goal是高层请求;capabilities是从AgentRegistry.get_capabilities_description()返回的关于代理及其输入输出的人类可读目录。
接下来,Planner为LLM准备简报。这个提示词同时承担两个工作:它列出了可用工具,并设定了制定计划的规则。你可以将其视为Planner在说:这里你可以使用的专家,这是如何引用之前的结果,以及这是要求的输出格式:
能力边界结束 ---
指令:
-
- 计划必须是一个JSON对象列表,其中每个对象代表一个“步骤(step)”。
-
- 你必须使用上下文链(Context Chaining)。如果某个步骤需要上一个步骤的输入,请使用语法
$$STEP_OUTPUT$$$来引用它。
- 你必须使用上下文链(Context Chaining)。如果某个步骤需要上一个步骤的输入,请使用语法
-
- 要具有策略性。将复杂目标(如顺序重写)拆分为不同的步骤。为写入代理(Writer agent)使用正确的键('facts' 对比 'previous_content')。
示例目标:“写一个关于阿波罗11号的悬疑故事。”
示例计划(JSON列表):
[{"step": 1, "agent": "Librarian", "input": {"intent_query": "悬疑叙事蓝图"}}, {"step": 2, "agent": "Researcher", "input": {"topic_query": "阿波罗11号登陆细节"}}, {"step": 3, "agent": "Writer", "input": {"blueprint": "$$STEP_1_OUTPUT$$", "facts": "$$STEP_2_OUTPUT$$$"}}]
示例目标:“写一份关于朱诺号(Juno)的技术报告,然后用口语化的重写。”
示例计划(JSON列表):
[{"step": 1, "agent": "Librarian", "input": {"intent_query": "技术报告结构"}}, {"step": 2, "agent": "Researcher", "input": {"topic_query": "朱诺号任务技术"}}, {"step": 3, "agent": "Writer", "input": {"blueprint": "$$STEP_1_OUTPUT$$", "facts": "$$STEP_2_OUTPUT$$$"}}, {"step": 4, "agent": "Librarian", "input": {"intent_query": "口语风格"}}, {"step": 5, "agent": "Writer", "input": {"blueprint": "$$STEP_4_OUTPUT$$", "previous_content": "$$STEP_3_OUTPUT$$"}}]
简报准备就绪后,规划器(Planner)调用LLM作为推理伙伴。整个过程封装在一个健壮的try...except块中,以处理潜在的错误,例如来自LLM的格式不正确的JSON。在调用LLM并解析响应后,规划器会执行关键验证。它检查输出是否为有效列表。为了确保韧性,它还会
第 4 章
处理LLM可能将列表封装在字典中(例如"plan": [...])的情况,并据此提取列表。如果结构不正确或发生任何其他错误,except块将记录详细的错误信息(包括用于便于调试的原始LLM输出),并抛出异常以停止引擎。成功解析并验证的计划将返回给执行器(Executor)使用:
plan_json = ""
try:
plan_json = call_llm_robust(system_prompt, goal, json_mode=True)
plan = json.loads(plan_json)
# 验证输出结构
if not isinstance(plan, list):
# 处理 LLM 将 List 封装在字典中的情况(例如 "plan": [...])
if isinstance(plan, dict) and "plan" in plan and isinstance(plan["plan"], list):
plan = plan["plan"]
else:
raise ValueError("规划器未返回有效的 JSON 列表结构.")
print("[Engine: Planner] 计划生成成功.")
return plan
except Exception as e:
print(f"[Engine: Planner] 生成有效计划失败。错误: {e}。原始 LLM 输出: {plan_json}")
raise e
这种设计之所以有效,是因为它为LLM提供了一个明确的可遵循的标准。接下来,我们将看到执行器如何将此计划转化为行动。
执行器 (Executor)
执行器是系统的现场经理:它运行每个步骤,调用正确的代理,最重要的是推进上下文,以便后续步骤可以建立在早期结果的基础上。
在调用代理之前,执行器会在输入中查找占位符(诸如$$ . . $$)。这些标记充当对之前结果的引用,告诉系统:“在此处使用步骤 1 的输出”。通过解析这些占位符,执行器将计划转换为一个连通的工作流。
这就是上下文链的实际应用。resolve_dependencies()辅助函数驱动了这一过程。它扫描代理所需输入中的任何$$$占位符。
该函数的外部部分定义了resolve_dependencies并创建输入参数的深拷贝。这确保了在替换占位符时,原始计划结构(在其他地方需要)不会被意外修改(变异):
def resolve_dependencies(input_params, state):
"""
辅助函数,用执行状态中的实际数据替换 $$$占位符。
"""
# 使用 copy.deepcopy 确保原始计划结构不被修改
resolved_input = copy.deepcopy(input_params)
内部解析器(基础情况)解析嵌套函数。它的“基础情况”是处理单个值链的逻辑。它检查字符串是否为占位符(以$$开头并以$$结尾)。如果是,它会在state字典中查找引用键(例如STEP_1_OUTPUT)并返回实际数据:
# 处理潜在嵌套结构的递归函数
def resolve(value):
if isinstance(value, str) and value.startswith("$$") and value.endswith("$$"):
ref_key = value[2:-2]
if ref_key in state:
# 从上一个步骤的输出中检索实际数据(字符串)
print(f"[Engine: Executor] 正在解析 {ref_key}")
return state[ref_key]
else:
raise ValueError(f"在 state 中未找到依赖 {ref_key}.")
(注:原文此处存在大量重复的段落,翻译时已按逻辑保留核心内容并确保格式一致)
执行追踪器 (Execution Tracer)
在规划器和执行器的协同工作,执行追踪器(Tracer)已经具备了功能,但尚未开始工作。让我们仔细观察。
当新任务开始时,追踪器用目标初始化。
class ExecutionTrace:
"""
记录 LLM 计划的执行情况。
用于调试和分析,让我们能够准确看到系统是如何推理并执行任务的。
"""
def __init__(self, goal):
self.goal = goal
self.plan = None
self.steps = []
self.status = "Initialized"
self.final_output = None
self.start_time = time.time()
过程完成后,追踪器使用finalize()将其封装。它记录了最终状态,存储了最终输出,并计算了任务所需的时间。
一旦规划器创建了策略,追踪器就会使用log_plan()进行记录。该方法存储整个计划,为我们提供了执行开始前系统意图执行的操作记录:
def log_plan(self, plan):
self.plan = plan
当执行器遍历计划时,log_step()记录每一个动作。对于每个步骤,它捕获了了哪个代理、接收了什么输入(planned_input)、在什么上下文中运行(resolved_context)以及输出了什么(output)。这创建了计划如何执行的清晰时间化记录:
def log_step(self, step_num, agent, planned_input, mcp_output, resolved_context):
"""记录单个执行步骤的细节."""
self.steps.append({
"step": step_num,
"agent": agent,
"planned_input": planned_input,
"resolved_context": resolved_context,
"output": mcp_output['content']
})
def finalize(self, status, final_output=None):
self.status = status
self.final_output = final_output
self.duration = time.time() - self.start_time
这些方法共同构成了追踪器的严密审查。它让我们能够在任何时候重构系统的推理过程,无论我们是在调试故障、分析性能,还是仅仅研究工作流是如何展开的。本质上,执行追踪器为上下文引擎提供了每个自主系统所需要的要素:可见性、问责制,以及我们始终能看到其思考方式的安心感。
整合一切
现在所有的构建模块都已就绪:负责思考的规划器 (Planner)、负责执行的执行器 (Executor)以及记录每一次移动的追踪器 (Tracer)。最后是将它们连接到单一控制循环中。
context_engine()函数是系统的开关。它通过协调整个两个阶段的过程让一切运转起来:它先制定计划,然后执行计划。让我们逐步分析。
当一个新任务开始时,该函数会打印目标并创建一个执行追踪器的实例,追踪器会立即开始记录。它还会加载代理注册表(Agent Registry),这是一个告诉系统它拥有哪些引擎以及如何访问它们的目录。从这一刻起,每一个动作、决策和输出都将被记录在追踪(trace)中。
def context_engine(goal):
"""
Context 引擎的主入口。管理规划与执行。
"""
print(f"\n=== [Context Engine] Starting New Task ===\nGoal:{goal}\n")
trace = ExecutionTrace(goal)
registry = AGENT_TOOLKIT
接下来是规划(阶段 1):
## Phase 1: Plan
try:
capabilities = registry.get_capabilities_description()
plan = planner(goal, capabilities)
trace.log_plan(plan)
except Exception as e:
trace.finalize("Failed during Planning")
# 即使失败也返回追踪信息以便调试
return None, trace
引擎获取用户的目标并开始制定计划。它从注册表中检索代理能力的描述,并将其交给规划器,规划器使用大语言模型(LLM)创建一个JSON格式的行动计划。该计划将目标分解为不同的、有序的步骤,描述了哪个代理应该做什么以及按什么顺序执行。一旦规划完成后,追踪器就会记录计划,在执行开始前保留策略。如果在此阶段发生任何错误(例如格式错误或规划失败),系统会整洁地记录并带有完整的追踪信息退出。
计划就绪后,引擎进入执行阶段(阶段 2):
print(f"\n[Engine: Executor] Starting Step (step_num): {agent_name})")
try:
handler = registry.get_handler(agent_name)
# 上下文组装:解决依赖关系
resolved_input = resolve_dependencies(planned_input, state)
# 通过 MCP 执行代理
# 为代理创建一个包含已解析输入*的 MCP 消息
mcp_resolved_input = create_mcp_message("Engine", resolved_input)
mcp_output = handler(mcp_resolved_input)
# 更新状态日志追踪
output_data = mcp_output["content"]
# 存储输出数据(上下文本身)
state[f"STEP_{step_num}_OUTPUT"] = output_data
trace.log_step(step_num, agent_name, planned_input, mcp_output, resolved_input)
print(f"[Engine: Executor] Step {step_num} completed.")
except Exception as e:
error_message = f"Execution failed at {step_num}({agent_name})"
print(f"[Engine: Executor] ERROR: {error_message}")
trace.finalize(f"Failed at Step {step_num}")
# 返回追踪信息以调试失败原因
return None, trace
这个循环是Context引擎实际工作的地方。执行器遍历计划的每一步,为任务调用相应的代理。在调用代理之前,它会使用resolve_dependencies()检查输入中的任何占位符(例如对早期步骤输出的引用),并将它们替换为存储在当前状态中的实际数据。然后,将输入封装成MCP消息并发送给代理,代理的响应将成为该步骤的输出。每个输出都存储在状态字典中,创建了一个后续步骤可以引用的短期记忆。追踪器会记录每一个细节:调用了哪个代理、接收了什么输入、解析了什么上下文以及产生了什么结果。
当所有步骤运行完成后,引擎会完成流程:
final_output = state.get("STEP_{len(plan)}_OUTPUT")
trace.finalize("Success", final_output)
print("\n==[Context Engine] Task Complete ===")
此时,最后一步的输出会被检索并返回。通过标记运行成功、记录结果并测量从开始到结束的时长,该函数返回生成的输出和完整的执行追踪。
运行引擎
我们已经构建了拼图的每一个碎片:专家、注册表、规划器、执行器和追踪器。在完成这些架构工作后,是时候启动并听引擎运行了。让我们看看当Context引擎被要求处理一个创意目标时会发生什么。我们将从一个简单的提示开始,要求一个关于阿波罗11号登月的悬疑故事:
print("******** Example 1: STANDARD WORKFLOW (Suspenseful Narrative)")
goal_1 = "为一个关于阿波罗11号登月的儿童故事写一个简短的悬疑场景,突出危险性。"
# 运行 Context 引擎
# 确保 Pinecone 索引已填充(参考第 3 笔记本)以便此工作
result_1, trace_1 = context_engine(goal_1)
if result_1:
print("\n******* FINAL OUTPUT 1 ***********\n")
display(Markdown(result_1))
print("\n\n" + "*"*50 + "\n\n")
# 可选:显示追踪以查看引擎的过程
trace_1.display_trace()
组装 Context 引擎
当此目标传递给引擎时,规划器开始行动:
-
- 它推理认为,为了创建一个突出“危险性”的悬疑场景,首先需要一个风格指南。因此,它的第一个规划步骤是调用管理员来获取悬疑风格的蓝图。
-
- 接下来,它识别出事实部分“阿波罗11号登月”,并知道需要准确的细节。它的第二个步骤是调用研究人员来获取相关事实。
-
- 最后,在兼顾风格和内容的计划下,它创建了第三步:调用作家生成最终内容,将第 1 步的蓝图与第 2 步的事实相结合。
靴子悬停在空中。我先看到了影子。在我落下之前,它先吻了表面。接着是接触。第一步。在月球上。世界与我同在,遥远又无处不在。宇服里的空气在低语。表面承住了重量。这一刻被拉得很长,像一个音符,没有破碎。
在生产环境中,你可以决定只向管理员显示执行轨迹,或将输出保存到执行日志中。或者,你也可以决定在训练阶段让用户看到执行轨迹,以理解上下文引擎(Context Engine)。这可以在实施期间的研讨会上决定。
随着这次成功执行,我们现在可以退一步,欣赏我们构建的内容。每一个架构组件都在协同工作,将抽象的意图转化为结构化、可追踪且有意义的输出。让我们回顾这一架构飞跃,以及使其在实践中运行如此优雅的设计原则。
总结
本章解决的核心挑战是预定义工作流的僵化。目标是创建一个高级控制器,通过对可用工具进行推理来动态执行复杂任务。这段旅程始于构建规划器(Planner),它作为战略核心,创建实现目标的逐步配方。然后我们构建了执行器(Executor),即执行计划的运营经理。最后,包含了一个追踪器(Tracer),以确保整个过程透明且可调试。
引入的关键创新是规划阶段。在这里,规划器会咨询代理注册表(Agent Registry),该工具包描述了每个专家代理(管理员、研究员、作家)的能力。凭借这些知识,规划器使用外部 LLM 生成一个为用户特定高层目标量身定制的自定义、多步骤的 JSON 计划。这将“做什么”与“怎么做”分离开了。执行器遵循 JSON 计划,按正确的顺序调用专家代理。这一阶段实现了上下文链(Context Chaining),这在第一章中讨论过。这使得系统能够处理线性工作流无法处理的复杂依赖任务。
本章以一个功能完备、能够独立思考的上下文引擎告终。通过动态生成自己的工作流,系统不再是简单的流水线,而是一个智能且模块化的工作坊。在下一章中,我们将通过集成生产级功能(如上下文管理、安全防护和错误处理),对该引擎进行加固,以用于实际应用。
第 4 章
125
问题
-
- 执行器是否创建了实现用户目标的战略性、逐步的计划?(是或否)
-
- 代理注册表的主要目的是为了记录每个操作以供调试吗?(是或否)
-
- 文本中描述的上下文链是指一个代理的输出成为下一个代理的输入的过程吗?(是或否)
-
- 专家代理(管理员/Librarian、研究员/Researcher 和作家/Writer)是否在架构图中用蓝色表示?(是或否)
-
resolve_dependencies()函数是否允许代理查询向量数据库?(是或否)
-
- 规划器是否使用代理注册表中的
get_capabilities_description()方法来理解哪些工具可用?(是或否)
- 规划器是否使用代理注册表中的
-
context_engine()函数是通往外部 LLM 的主要接口吗?(是或否)
-
- 作家(Writer)代理是否处理程序化 RAG 以获取风格蓝图?(是或否)
-
- 整个过程首尾是否由代表用户目标和最终输出的橙色组件界定?(是或否)
-
- 所有代理是否都使用模型上下文协议(MCP)进行通信?(是或否)
参考文献
-
- Wei, J., Wang, X., Schuurmans, D., Bosma, M., Chi, E., Le, Q., & Zhou, D. (2022). Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. Chain-of-Thought Eliciting Reasoning in Large Language Models. arXiv preprint arXiv:2201.11903.
-
- Yao, S., Zhao, J., Yu, D., N, Ha., & Tsvetkov, Y. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv preprint arXiv:2210.03629.
-
- Schick, T., Dwivedi-Yu, J., Dessi, R., Raileanu, R., Romeli, M., Zettlemoyer, L., Cancedada, N., & Scialom, T. (2023). Toolformer: Language Models Can Teach Themselves to Use Tools. arXiv preprint arXiv:2302.04761.
续读
- Wu, Q., Bansal, G., Zhang, J., Wu, Y., Zhang, S., Zhu, E., Li, B., Jiang, L., Zhang, J., & Wang, C. (2023). AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation. AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation. arXiv preprint arXiv:2308.08155.
订阅免费电子书
新框架、演进中的架构、研究发布、生产环境分析——AI_Distilled 将噪音过滤为为亲手操作 LLM 和生成 AI 系统的工程师和研究人员提供的每周简报。现在订阅即可获得免费电子书,以及有帮助的每周见解。
5
加固上下文引擎
在第 4 章中,我们构建了上下文引擎的原型。然而,为了从实验转向生产级系统,我们必须加固引擎。本章侧重于使系统变得健壮、安全且可扩展所需的升级。
转换跨越三个阶段展开:
-
初始化(Initiation):每个操作都从用户操作开始。
-
规划(Planning):引擎创建策略。
-
执行(Execution):专家代理执行任务。
我们从代码的逐步讲解开始。
第一阶段:初始化
每个操作都从用户操作开始,即目标被提交且引擎启动的时刻。
logging.info:第一个动作是记录过程的开始。这是任何生产导向系统中跟踪任务的简单但至关重要的一步。
第二阶段:规划阶段
一旦进入 context_engine(),首要任务就是创建策略。引擎并非盲目行动;它收集关于拥有哪些工具、用户想要以及如何桥接两者的信息。然后它起草了一份下游组件可以遵循的详细执行计划。
-
ExecutionTrace.__init__():引擎立即初始化一个ExecutionTrace对象。该对象充当引擎的“飞行记录器”,准备记录后续每个操作以供透明度和调试。 -
5.3
AgentRegistry.get_capabilities_description()(紫色):为了创建一个有效的计划,规划器(Planner)需要知道它拥有哪些可用工具。它查询AGENT_TOOLKIT对象以获取所有专家代理及其所需输入的纯文本格式描述。 -
6.2
planner()(白色):在获取用户目标和代理能力列表后,引擎调用planner()函数。 -
3.1
call_llm_robust()(橙色):planner()将推理任务委托给外部 LLM。LLM 在一个结构严谨的提示词(prompt)中理解目标和能力,并要求 LLM 返回一个具有策略性的、多步骤的 JSON 计划。 -
6.1b
ExecutionTrace.log_plan()(白色):一旦从 LLM 接收到 JSON 计划,它就会记录在我们的ExecutionTrace对象中。这完成了规划规划阶段。
第 3 阶段:执行循环
这是上下文引擎(Context Engine)展现活力的地方。有了计划后,系统开始按顺序执行每一步(这里的步骤指的是上下文引擎动态流中实际的、编号的步骤)。执行器(Executor)获取任务所需的正确流程,准备其输入并跟踪每一个输出,形成一个包含规划、执行和验证的持续反馈循环。
循环从执行器从计划中读取后续步骤开始:
-
5.2
AgentRegistry.get_handler()(紫色):计划通过名称指定了一个代理(例如 "Librarian"/管理员)。执行器使用get_handler()从注册表中检索实际的、可调用的 Python 函数(例如agent_context_librarian)。 -
6.3a
resolve_dependencies()(白色):这是上下文链式(context chaining)的核心。执行器检查代理所需的输入。如果它发现像$$STEP_1_OUTPUT$$这样的占位符,该函数将将其替换为步骤 1 产生的实际数据(上下文引擎动态流的第一步),这些数据存储在引擎的状态字典中。 -
4.x
agent-*(绿色):执行器现在调用检索到的代理函数,并向其传递完全解析的上下文。
在每个代理内部,专门的逻辑驱动着系统的解决问题行为。代理通常依赖辅助函数来访问外部资源并执行繁重工作:
-
管理员(Librarian)和研究员(Researcher)代理调用
3.4 query_pinecone()(橙色) 在向量数据库上执行语义搜索。该函数反过来使用3.2 get_embedding()(橙色) 将查询文本转换为向量。 -
研究员(Researcher)和作者(Writer)代理调用
3.1 call_llm_robust()(橙色) 来合成事实或生成最终内容。 -
6.1c
ExecutionTrace.log_step()(白色):一旦代理完成工作并返回输出,就会调用log_step()方法来记录有关完成步骤的所有信息:使用的代理、输入以及其最终输出。输出也会保存到引擎的状态中,以供后续步骤使用。
循环进入计划中的下一步,重复检索代理、准备其输入、执行步骤和记录结果的整个序列。
第 4 阶段:最终完成
最后一个阶段闭合了循环。在所有步骤都成功执行后,上下文引擎通过记录其最终状态并返回结果和追踪记录(trace)来结束其任务。这一阶段发生在平稳完成之后,交付一个清晰的结果以及关于实现该结果的透明记录。
-
6.1d
ExecutionTrace.finalize()(白色):引擎在追踪上执行finalize(),记录最终状态(“Success”/成功)和总执行时间。 -
7
context_engine()然后将最终输出(最后一步的结果)和完整的追踪对象返回给用户的脚本。 -
7.0b
logging.info& 7.1/7.2display(Markdown(...))(蓝色):脚本记录任务已完成,并向用户显示最终的格式化输出,成功实现了原始目标。
前述流程图中的每个颜色块都代表一个具有单一职责的定义良好的组件。它们共同构成了一个整洁的、分层的架构,这是可扩展多代理系统的重要要素特征:
-
引擎核心(白色)是大脑。它并不孤立工作,而是管理整个过程。它规划策略(
planner)、按步骤执行计划,并记录一切(ExecutionTrace)。它的主函数context_engine()是主编排器。 -
代理注册表(Agent Registry,紫色)是工具箱。它为两个主体服务:它为规划器提供一份描述性手册(
get_capabilities_description)以便其思考,为执行器提供实际工具(get_handler)以便其行动。 -
专家代理(绿色)是工作者。每个都有一个特定的任务:管理员寻找风格指令,研究员寻找事实,作者负责创建内容。它们是操作的执行手。
-
辅助函数(橙色)是实用程序。这些是共享的低级函数,任何其他组件都可以使用。它们处理重复但至关重要的任务,如与 LLM 通信(
call_llm_robust)和与向量数据库通信(query_pinecone),避免了重复并集中了关键交互。
通过分析这些,我们可以看到系统如何通过逻辑化、可预测且可追溯的简单操作序列来实现行为。现在我们已经详细探索了蓝图,准备好在代码中构建每个组件了。
为了在现实世界中的可扩展性进行重构
我们将原型转换为生产级应用程序的旅程从重构开始。在第 4 章中,我们的笔记本包含几个专门安装、初始化客户端的单元格。虽然这种方法可行,但它有两个缺点:它弄乱了我们的主工作区,并且迫使我们在每个笔记本中重复相同的代码。为了解决这个问题,我们现在将这些逻辑移动到 utils.py 中。
通过重构,我们将确保每个组件都从一致且可靠的环境开始。这不仅清理了我们的笔记本,还让我们能够专注于高层编排。我们正在从笔记本为中心的原型转向模块化系统。
在 utils.py 中创建函数
第一步是在 utils.py 中创建两个函数:一个处理依赖,另一个用于初始化客户端。
-
install_dependencies()函数充当项目的“一键安装器”。通过将所有的 pip 命令封装在这个函数中,我们保证每个笔记本都使用相同版本的库。这对于确保可重复性至关重要。 -
initialize_clients()函数是我们的密钥管理员。它集中了从 Colab Secrets 获取API_KEY和PINECONE_API_KEY的整个过程,并使用它们创建并配置 OpenAI 和 Pinecone 客户端。
这些函数共同为我们提供了可重用且安全的设置,使得项目的每个组件都准备就绪。
在本节中,我们将升级这些核心函数,使其更加可靠、可测试且透明。架构上的关键转变是实现了依赖注入。我们不再让函数去寻找全局变量,而是现在显式地将所有所需的组件(如客户端和配置)作为参数传递。这使得它们从简单的连接器转变为健壮的、自包含模块,能够优雅地处理故障、提供清晰的日志并高效地管理资源。让我们开始吧!
通过依赖注入增强模块化
我们助手函数最显著的升级是弃用全局变量。在我们的原型中,像 call_llm_robust 等函数隐式地知道应该使用哪个 client 和 GENERATION_MODEL,因为它们被定义在全局作用范围内。这对于原型来说很方便,但对于实际应用程序可能是危险的,因为它创建了隐藏依赖,并使得代码难以测试或重新配置。
我们的升级引入了依赖注入,这是一种函数的依赖项作为参数“注入”的做法。这使得每个函数成为一个自包含的单元。客户端或模型名称来自何处不再重要,只要在调用函数时提供它们即可。这使得我们的引擎变得灵活得多。例如,我们可以通过传递不同的 client 对象,同时使用两个不同的 LLM 运行两个不同的引擎任务。
通过生产级日志提高透明度
在原型中,我们使用 print() 语句来查看发生了什么。对于生产系统来说,这是不够的。专业系统需要正式的日志:一种结构化的、带有时间戳且可靠的事件记录。
我们最后的增强是将所有的 print() 语句替换为 Python 内置的 logging 模块。这是一个根本性的转变,提升了我们系统的透明度。适当的日志允许我们区分 INFO 信息、潜在的警告和严重错误。这种结构化的输出不仅便于人类阅读,而且是机器可读的,允许日志馈到自动化监控和警报系统中。
# === 配置生产级日志 ===
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
通过主动上下文管理提高效率
构建 LLM 应用的关键方面是管理 token 的“隐形成成本”。我们发送给模型的每个上下文都会消耗 token,这会影响成本和速度,并在超出上下文窗口时导致硬性故障。专业系统不能肆意地使用这些资源。
为了解决这个问题,我们引入了 count_tokens 工具函数。该函数像一个“燃料表”,允许引擎在发送提示之前衡量成本。它使用 OpenAI 官方的 tiktoken 库来确保准确性。现在构建它,为以后高级功能奠定了基础,例如自动总结上下文以保持在 token 预算内。
# === 上下文管理工具(新增) ===
def count_tokens(text, model="gpt-4"):
"""为给定模型计算字符串中的 token 数量"""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
# 为可能不在 tiktoken 注册表中的模型回退
encoding = tiktoken.get_encoding("cl100k_base")
return len(encoding.encode(text))
这个简单的工具函数是让引擎不仅强大,而且具有成本意识且高效的关键一步。
升级后的助手函数演示
这是我们核心助手函数的最终加固代码。注意每个签名如何包含了参数如 client、generation_model 和 embedding_model。每条语句都已替换为日志调用,且错误处理更加具体。
# === LLM 交互(使用依赖注入加固) ===
@retry(wait=random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def call_llm_robust(
system_prompt, user_prompt, client, generation_model,
json_model=False
):
处理所有带重试的 LLM 交互的集中函数。升级:现在要求传入 client 和 generation_model 对象。
logging.info("尝试调用 LLM...")
try:
response_format = {"type": "json_object"} if json_model else {"type": "text"}
# 升级:使用传入的 client 和模型名称进行 API 调用。
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 调用成功.")
return response.choices[0].message.content.strip()
except APIError as e:
logging.error(f"call_llm_robust 中的 OpenAI API 错误: {e}")
raise e
except Exception as e:
logging.error(f"call_llm_robust 中发生意外错误: {e}")
raise e
嵌入向量(使用依赖注入加固)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def get_embedding(text, client, embedding_model):
为单个文本查询生成带重试的嵌入。升级:现在要求传入 client 和 embedding_model 对象。
text = text.replace("\n", " ")
try:
response = client.embeddings.create(input=[text], model=embedding_model)
return response.data[0].embedding
except APIError as e:
logging.error(f"get_embedding 中的 OpenAI API 错误: {e}")
raise e
except Exception as e:
logging.error(f"get_embedding 中发生意外错误: {e}")
raise e
将此代码移动到最终的 agents.py 暴露了一个微妙但重要的缺陷,以及我们如何修复它以创建最终的、生产就绪的版本。
正如我们将看到的,在最终版本执行第一版本时,agent 的输出包含一个关键缺陷。重要的是要意识到这类问题是开发 AI agent 过程中的自然部分,而系统地解决这些问题正是使系统具有韧性的关键。中间实现中的关键行显示了 agent 升级后的签名及其原始的、错误的返回语句:
# === 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。这使得 agent 成为一个自包含的、可测试单元。 -
我们还规范了日志记录。所有的
print语句都已替换为专业的日志调用。 -
我们开发了健壮的错误处理流程。整个逻辑被封装在
try...except块中,以优雅地处理失败。
在此过程中,早期版本中的缺陷变得清晰可见了:它以原始 JSON 字符串的形式返回蓝图。
为了确保系统健壮,每个 agent 必须以一种预测的、结构化的方式返回数据。例如,Writer agent 必须确切知道在 Librarian 的输出中查找哪个键。修复方法是将输出封装在一个具有一致键 "blueprint_json" 的字典中。这在 agent 之间建立了一个稳定性的数据契约。
代码的最终修正版本现在位于 agents.py 中,反映了这一关键修复以及其他的专业升级。
最终加固代码
我们现在有了最终代码,并进行了拆分以显示每个升级的实现位置。首先,我们定义了加固后的函数签名。所有依赖现在都是显式参数,而不是全局变量,使 agent 成为一个自包含的单元:
def agent_context_librarian(
mcp_message, client, index, embedding_model, namespace_context
):
"""从 Context Library 中检索相应的 Semantic Blueprint."""
接下来,我们用专业的日志替换了旧的 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 content")
except
对 query_pinecone 辅助函数的调用现在升级为使用显式的关键字参数以及注入 agent 的所有必要客户端和对象模型:
results = query_pinecone(
query_text=requested_intent,
namespace=namespace_context,
top_k=1,
index=index,
client=client,
embedding_model=embedding_model
)
最后,我们实现了在验证阶段发现的最关键的修复。agent 的输出现在是一个具有一致键的字典,为其他 agent 创建了可靠的数据契约:
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 agent
与我们完善 Librarian 的方式相同,Researcher 遵循相同的增量改进模式,从笔记本加固到库集成。该过程始于 Context_Engine_MAS_MCP.ipynb,引入了依赖注入和结构化日志,并随后模块化到 agents.py 文件。
与 Librarian 类似,这个版本的版本暴露了一个微妙但关键的缺陷。在验证期间,我们发现 agent 返回的是原始字符串而不是结构化数据,这是一个很容易犯的错误,但它会影响整个工作流:
# == 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
第 5 章 141
此阶段的升级在结构上与应用于 Librarian 的升级相似:
-
我们通过显式要求所有外部需求(
client、index、generation_model和namespace_knowledge)加强了依赖注入 -
我们将
print语句替换为专业的日志 -
我们将逻辑封装在
try...except块中以进行健壮的错误处理
在此过程中,早期版本中的缺陷变得清晰可见:它以原始字符串的形式返回蓝图。
写入代理 (Writer agent)
写入代理完成了我们专家组合的最后成员,它的重构展示了系统的最终集成。与管理员(Librarian)和研究员(Researcher)一样,它最初在 Context_Engine_MAS_MCP.ipynb 中通过结构化日志、错误处理和依赖注入进行了加固。
这次升级揭示了关于集成系统最重要的教训:当你更改一个组件时,必须更新依赖于它的组件。记住,从设计到完美的道路没有捷径。因此,知道如何解决问题与开发功能同样重要!写入代理的初始加固版本与旧的管理员和研究员配合,但它包含一个关键缺陷,使其无法与其新的升级版本协同工作。
该中间实现中的关键行显示了代理在输入处理上的缺陷:
# === 4.3. Writer Agent (Upgraded) ===
def agent_writer(mcp_message, client, generation_model):
"""将研究结果与蓝图结合以生成最终输出."""
logging.info("[Writer] Activated. 正在将蓝图应用于源素材...")
try:
# Fix:从之前的步骤中解包结构化输入
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,使其成为一个自包含的单元。 -
所有的打印语句都被替换为专业的日志调用。
-
整个逻辑封装在
try...except块中,以优雅地处理失败。
让我们看看如何修复集成流。
写入代理处理输入的方式存在缺陷。它期望原始字符串,而不是包含 "blueprint_json" 和 "facts" 的结构化字典。为了修复这个问题,我们升级了写入代理以智能地解包这些结构。这不仅仅是一个错误修复;它是创建真正互连且稳健的多代理系统的最后一步,使数据能够可预测地从一个组件流到下一个。出现在 agents.py 中的最终修正版代码反映了这一关键的集成修复。
最终加固代码
这是最终代码,按每个升级实现的位置进行了拆分。首先,我们加固了下一个函数签名和初始日志设置:
def agent_writer(mcp_message, client, generation_model):
"""将研究结果与蓝图结合以生成最终输出."""
logging.info("[Writer] Activated. 正在将蓝图应用于源素材...")
try:
接下来,我们实现了最关键的修复。这段代码智能地解包结构化输入,检查它们是字典(来自我们新的代理)还是原始字符串(为了向后兼容或其他用途),使代理变得健壮:
blueprint_data = mcp_message['content'].get('blueprint')
facts_data = mcp_message['content'].get('facts')
previous_content_data = mcp_message['content'].get(
'previous_content'
)
# 提取实际字符串,同时处理字典和原始字符串输入
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 # 假设这已经是字符串
if not blueprint_json_string:
raise ValueError("Writer 要求输入内容中包含 'blueprint'。")
代理的其余逻辑保持不变,但它现在在正确解包的 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 需要 "facts" 或 "previous_content"。')
# ... (系统提示和用户提示词的构建保持不变) ...
最后,代理对 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] 发生错误: {e}")
raise e
随着整个代理团队现在已升级到生产标准,我们可以继续重构管理它们的组件。
重构代理注册表 (Agent Registry)
我们将注意力转向AgentRegistry,它是作为我们引擎总工的组件,持有主剪事板。在第 4 章中,它作为一个简单的路由器,将代理名称映射到它们的函数。现在,我们将其升级为智能中心工具箱。
它的升级是加固组件的最后也是最关键的一步,因为它负责正确管理并向所有专门代理注入依赖项。与其他组件一样,我们首先在Context_Engine_MAS_MCP.ipynb笔记本中对其进行升级。初始升级重点在于增强get_handler方法,以管理新要求的依赖项(如client、index和namespace配置)。然而,与之前的重构一样,第一遍暴露了一个根本性的架构缺陷。笔记本版本直接引用代理函数,这种捷径只在单文件环境中有效:
class AgentRegistry:
def __init__(self):
加固上下文引擎 (Context Engine)
self.registry = {
# 为函数名添加 "agents." 前缀
"Librarian": agents_context_librarian,
"Researcher": agent_researcher,
"Writer": agent_writer,
}
在此阶段,get_handler方法已经显著改进。它接受所有关键依赖(client、index、generation_model等),并使用 lambda 函数为每个代理生成处理器。但一旦将注册表迁移到.py文件,出现了一个熟悉的问题。正如我们模块化代理一样,原始代码假设函数存在于全局命名空间中,这导致重构后的模块出现NameError。解决方案(类似于管理员和研究员的修复)是让注册表自包含。它需要显式导入代理,例如agents.context_librarian。这个微小但重要的更改确保了组件确切依赖的位置,建立了完全模块化的架构。
registry.py中的最终修正版代码反映了这一改进。
最终加固代码
这是最终代码,按每个升级实现的位置进行了拆分。首先,注册表正确导入了依赖:
import logging
import agents
接下来,在__init__方法内部,它使用agents.前缀引用函数,解决了模块化问题:
class AgentRegistry:
def __init__(self):
self.registry = {
# 为函数名添加 "agents." 前缀
"Librarian": agents.agent_context_librarian,
"Researcher": agents.agent_researcher,
"Writer": agents.agent_writer,
}
get_handler方法保持我们在笔记本中构建的强大注入器,但它现在在正确导入的代理函数上运行:
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_name}")
raise ValueError(f"在注册表中未找到代理 {agent_name}")
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
升级中心编排器
随着代理(agents)和注册表(registry)的强化和模块化,我们将转向架构的核心:上下文引擎(Context Engine)本身。作为系统的中央“大脑”,它将所有组件整合成一个连贯的思考机器。
在第 4 章中,我们构建了驱动此编排循环的核心元素。在本节中,我们将升级其中的每一个元素,以满足我们新的生产就绪标准。上下文引擎作为整个应用的中央神经系统,包含了三个关键组件:ExecutionTrace类(追踪器)、planner 函数以及context_engine编排器。
从Context_Engine_MAS_MCP.ipynb迁移到特定的engine.py标志着我们向模块化生产系统转型的最后也是最重要的一步。虽然ExecutionTrace等单个类在笔记本中已经过加固,但它们仍然保持单体结构是一个关键缺陷。为了让引擎具有可重性和维护性,其逻辑必须与笔记本环境完全解耦。
正如我们在其他组件中看到的那样,从单笔记本原型转向模块化代码会暴露隐式依赖。上下文引擎的笔记本版本假设所有支持模块(代理、助手和注册表)都于相同的作用域内。一旦我们将引擎移动到自己的文件中,该假设就失效了:
class ExecutionTrace:
# ... (Tracer Logic 已经健壮) ...
def planner(goal, capabilities):
# ...
def context_engine(goal, ...):
# 缺陷:此函数直接调用 planner 等其他函数
# 并使用 AGENT_TOOLKIT,假设它们是全局可用的。
遵循应用于代理和注册表的相同流程,引擎现在已移动到它自己的文件engine.py中。这种模块化不仅修复了架构缺陷,还将上下文引擎建立为一个自包含的组件,它显式地导入其所依赖的所有内容。
第 5 章
engine.py文件现在导入了助手函数和注册表(注册表本身导入了代理)。
结果是一个独立的引擎,它可以被导入到任何笔记本中或直接集成到更大的应用程序中,实现了我们构建真正的生产级、可重用系统的目标。
接下来所示的最终修正版代码代表了一个完整的、端到端的编排层,它将所有组件整合在一起。
最终加固后的代码
这里是最终代码,经过分解以显示其关键组件。首先,我们有了模块化的导入,它们修复了单体笔记本的核心架构缺陷。
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:
"""记录整个执行流以供调试和分析"""
# ... (提供了完整的类代码) ...
planner 也被移动到了模块中。它作为战略核心,使用 LLM 生成分步骤的计划。
def planner(goal, capabilities, client, generation_model):
"""分析目标并使用 LLM 生成结构化的执行计划(Execution Plan)。"""
# ... (提供了完整的 planner 逻辑) ...
最后,context_engine函数充当主编排器或执行器。它初始化追踪器、规划器,并遍历计划的每一步,使用AGENT_TOOLKIT获取正确的代理处理器并执行任务。这是赋予整个计划生命力的引擎核心。
def context_engine(
goal, client, pc, index_name, generation_model, embedding_model,
):
"""上下文引擎的主入口。"""
# (提供了完整的执行循环逻辑) ...
随着引擎的大脑现在已完全升级,整个框架已经加固并准备就绪。本章的最后一步是通过实际运行来测试我们新重构的系统。
运行加固后的引擎
我们成功地对上下文引擎的每个组件进行了重构和加固。我们提升了代理的可靠性,模块化了依赖注入,创建了智能代理注册表(Agent Registry),并将核心逻辑整合到context_engine编排器中。现在,真理时刻已经到来。是时候初始化整个系统并观察它了。
在本节中,我们将运行完全组装的引擎。我们不仅是在测试它是否可行;我们是在证明我们的架构升级带来了一个透明、动态且多步推理的系统。
可视化追踪
我们的engine.py模块现在已经完成。然而,还需要向笔记本环境中添加最后一个实用工具。日志提供了详细的审计记录,但引擎行为的快速视觉化总结对于分析分析是有无价的。
为了实现这一点,我们将保持engine.py库整洁,并在笔记本内部定义一个名为execute_and_display的专用演示函数。该函数作为我们的“引擎室”,处理整个过程:调用引擎,接收最终结果和详细追踪对象,并对两者进行格式化以实现整洁的人类可读显示。这种方法将核心引擎逻辑与面向用户的输出严格分离。
它将集成到最后的生产前笔记本章节。现在让我们运行标准执行。
标准工作流执行
我们的第一个测试使用了一个简单、熟悉的目标,与第 4 章的示例类似。我们将要求引擎写一个悬疑场景,这自然会触发经典的“图书管理员(Librarian)→ 研究员(Researcher)→ 作者(Writer)”工作流。
此测试的目标是确认重构后的引擎能够处理多步工作流。我们将在单元格中定义目标:
goal = "写一个悬疑场景,这自然会触发经典的 图书管理员 → 研究员 → 作者工作流。"
execute_and_display(goal, client, pc)
输出提供了海明风格的响应:
--- OUTPUT ---
这里是阿波罗11号的快速简报
- 首次载人登月,往返约8天。从肯尼迪航天中心(LC-39A)发射。
- 日期:1969年7月16日发射;7月20日02:17:40 UTC 着陆;7月21日02:56:15 UTC 第一步;7月24日溅水。
- 船员:尼尔·阿姆斯特朗(指挥官),巴兹·奥尔德(月球舱)
然后我们得到追踪:
--- TRACE (for the reader) ---
Trace Status: Success
Total Duration: 149.33 seconds
...
复杂工作流执行
我们的第二个测试将引擎推向简单的线性工作流。这次,我们将赋予它更复杂的目标:首先写阿波罗11号登陆的事实总结;然后用简约、富有冲击力的海明风格重写。
这种场景迫使两个代理链在一起,并将第一个的输出作为第二个的输入。
goal_2 = "首先,写阿波罗11号登陆的事实总结。然后,用简约、富有冲击力的海明风格重写。"
execute_and_display(goal_2, client, pc)
输出显示:
--- OUTPUT ---
月球是灰色而寂静的。人们进入舱。尘土飞扬了。阿姆斯特朗走了出去。这是小小的一步。世界是寒冷而遥远的。
第5章
[Engine: Planner] 正在分析目标并生成执行计划...
[Engine: Planner] 计划生成成功。
此初始区块确认引擎已接收目标。规划器(Planner)立即激活,分析请求,调用代理注册表(Agent Registry)获取可用工具,并制定了一个实现目标的三个步骤的JSON计划。这是引擎的思考阶段。
执行步骤 1 – 图书管理员 (The Librarian)
[Engine: Executor] 正在启动第1步:图书管理员
[Librarian] 已激活。正在分析意图...
[Librarian] 找到蓝图 'blueprint_suspense_narrative' (得分: 0.66)
[Engine: Executor] 第1步完成。
执行器(Executor)现在开始根据计划行动。它带有输入意图intent_query: "suspful children's story"激活图书管理员代理。管理员随后将其转换为向量,并在 Pinecone 的 Contexti 库命名空间中搜索。它找到了一个匹配的蓝图blueprint_suspense_narrative,其中包含了如何以悬疑语调写作的指令(例如:使用短句、关注感官细节、营造紧张气氛)。
执行步骤 2 – 研究员 (The Researcher)
[Engine: Executor] 正在启动第2步:研究员
[Researcher] 已激活。正在调查主题...
[Researcher] 找到2个相关片段。正在综合中...
[Engine: Executor] 第2步完成。
接下来,执行器带有输入主题查询topic_query: "Apollo 11 moon landing danger"激活研究员。研究员搜索 KnowledgeStore 命名空间并检索出两个与阿波罗11号任务相关的事实片段。然后,它使用 LLM 将这些事实合成一个简洁的摘要,作为其输出。
执行步骤 3 – 作家 (The Writer)
[Engine: Executor] 正在启动第3步:作家
[Engine: Executor] 已解析依赖项 STEP_1_OUTPUT。
[Engine: Executor] 已解析依赖项 STEP_2_OUTPUT。
[Writer] 已激活。正在将蓝图应用于源材料...
[Engine: Executor] 第3步完成。
这是最后也是最关键的一步。执行器首先执行上下文链(context chaining)。它用来自管理员的悬疑蓝图解析$$STEP_1_OUTPUT$$占位符,并用研究员的事实摘要解析$$STEP_2_OUTPUT$$占位符。随后激活作家代理,接收风格指令和事实内容作为输入。它将两者结合以生成最终文本。
最终化
随着最后一步完成,引擎任务成功结束。
=== [Context Engine] 任务完成 ===
最终输出是三个代理工作流的直接产物。它是请求风格与经过验证的作品的合成,完美和谐:
******** 最终输出 1 ********
1969年7月20日。阿波罗11号。NASA阿波罗计划。太空竞赛在背景中嗡嗡,宛如远处的风暴。
我们有三个人。尼尔·阿姆斯特朗。巴兹·奥尔德林。迈克尔·柯林斯。
月球填满了窗户。黑色的天空。硬光。长影。
嗡嗡声升起,又落下。鹰号稳住了。场景收缩到针尖大小。
接着是寂静。
一只靴子悬空着。我先看着影子。在我之前,它先吻了表面。
接着是接触。第一步。在月球上。
...
这一刻拉得很长,像一个音符,没有断裂。
让我们分析一下输出:
-
图书管理员的影响(风格): 输出完美匹配了
blueprint_suspense_narrative。句子简短、陈述性且具有冲击力(“舱内很小。”“燃料不足。”)。它专注于感官细节(“耳边轻微的嘶声”、“脚下的震抖”)并使用了唤发情感、营造紧张感的语言(“场景收缩到针尖大小”、“瞬间拉得很长……没有断裂”)。这是蓝图风格指导的体现。 -
研究员的影响(事实): 故事基于研究员检索的事实信息。它正确地提到了日期(1969年7月20日)、任务名称(阿波罗11号)、机组人员(阿姆斯特朗、奥尔德林、柯林斯)以及登月舱的名称(鹰号)。
此输出清楚地证明了强化后的上下文引擎正按预期工作。成功成功规划了一个动态工作流,协调了多个代理,并通过上下文链产生了一个连贯的结果。我们已经正式从一个机明的原型进化为一个健壮、有韧性且透明的系统。下一步是让它的使用变得真正无缝。
引擎模块化 (Modularizing the engine)
随着上下文引擎的每个组件现在已通过强化,我们可以迈出原型到生产前旅程中最后也是最重要的一步:为了模块化进行重构。在此阶段,我们将通过将核心逻辑移动到单独的 Python 库,将单体笔记本转换为整洁、可扩展的应用程序。
这个过程不仅仅是简单的复制粘贴。这是一次实用的调试之旅,揭示了 Python 模块如何通信、管理依赖以及交换数据。我们将从一个干净的笔记本 Context_Engine_Pre_Production.ipynb 开始,它作为我们的控制台,并将强化后的代码组织到专门的 commons/library 中。
我们的目标是将 Context_Engine_MAS_CP.ipynb 笔记本中第 3、4、5、6 部分的代码分离到自己的 .py 文件中:helpers.py、agents.py、registry.py 和 engine.py,它们将位于新的 commons/ 目录下。重构标志着。
本地导入 (Local imports)
我们面临的第一个挑战是 commons/ 的导入语句将失败。
# 这对于库是有效的
import helpers
import agents
from engine import Engine
解决方案是显式地连接模块。我们编辑 registry.py 以匹配。
# 告诉这个文件
import agents
import logging
class AgentRegistry:
def __init__(self):
self.registry = {
# 指定找到每个
"Librarian": agents.agent_librarian,
"Researcher": agents.agent_researcher,
"Writer": agents.agent_writer,
}
# ... 类的其余
这种识别依赖项的过程持续到。engine.py 需要导入我们的模块,而 agents.py 需要从 helpers.py 导入函数。
模块独立 (Module independence)
运行新的单元格立即一个孤岛。模块对……无所知。
缺失代理 (Missing agents)
导入 registry.py 时发生第一个错误:
NameError: name 'agent_context_librarian' is not defined
数据结构不匹配 (Mismatch of data structures)
在解决所有导入错误后,引擎在最后一步(例如 {'blueprint_json': '...'})之间期望源材料是原始字符串。作家的最终输出不是故事,而是错误。
修复方法是让作家代理更智能。我们修改它以检查输入(蓝图和事实)是否为字典。如果是,它将从正确的键提取字符串值。这确保了无论内部数据结构如何,代理都能获取其所需的文本。
agents.py 中修改后的 agent_writer 代码如下:
# 修复:从之前的步骤解包结构化输入
blueprint_data = mcp_message['content'].get('blueprint')
facts_data = mcp_message['content'].get('facts')
previous_content = mcp_message['content'].get('previous_content')
# 提取实际字符串,处理字典和原始输入
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 # 假设如果提供的则已经是字符串
最终生产前笔记本 (The final pre-production notebook)
我们最后的笔记本 Context_Engine_Pre_Production.ipynb 现在非常整洁,作为我们强大引擎的高级“控制台”。它包含两个主要部分。
中心化执行函数(引擎机房)
第一部分定义了一个单一函数 execute_and_display(),它封装了运行引擎和展示结果的所有逻辑。它接收用户目标、一个配置字典以及初始化的客户端,然后为用户打印一份精化的最终输出,并为开发者打印一份详细的技术轨迹追踪(technical trace)。
# === 引擎机房:主执行函数 ===
# 此函数包含了运行引擎的所有逻辑。
# 我们在这里定义它,是为了让最后的单元格代码保持非常简单。
import logging
import pprint
from IPython.display import display, Markdown
def execute_and_display(goal, config, client, pc):
"""
使用给定的目标和配置运行上下文引擎,
然后显示最终输出和技术轨迹追踪。
"""
logging.info(f"******* Starting Engine for Goal: '{goal}' ***********\n")
# 1. 使用提供的配置运行上下文引擎
result, trace = context_engine(
client=client,
pc=pc,
**config # 将配置字典解包为关键字参数
)
# 2. 为主读者显示最终结果
print("--- FINAL OUTPUT ---")
if result:
display(Markdown(result))
else:
print(f"The engine failed to produce a result. Status: {trace.status}")
# 3. 为开发者/技术读者显示技术轨迹追踪
print(f"\n\n--- TECHNICAL TRACE (for the tech reader) ---")
if trace:
print(f"Trace Status: {trace.status}")
第 5 章
159
print(f"Total Duration: {trace.duration:.2f} seconds")
print(f"Execution Steps:")
# 使用 pprint 处理整洁、可读的字典
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(trace.steps)
现在有了中心化的执行流程。让我们探索用户交互。
用户交互(控制台)
最后的调用是用户唯一需要交互的接口。在这里,我们定义了高层目标和一个包含运行所需所有技术参数的配置字典(例如模型名称和 Pinecone 命名空间)。这种关注点的清晰分离——将逻辑放在引擎机房,将用户输入放在控制台——是良好设计应用的标志。
配置位于用户目标上方的单元格中:
# 1. 在字典中定义本次运行的所有配置变量
config = {
"index_name": "genai-mas-mcp-ch3",
"generation_model": "gpt-5",
"embedding_model": "text-embedding-3-small",
"namespace_context": "ContextLibrary",
"namespace_knowledge": "KnowledgeStore"
}
控制台现在已经过优化:
# 示例 1
# 定义高层目标
goal = ["YOUR GOAL"]
# 调用上方单元格中的执行函数
execute_and_display(goal, config, client, pc)
系统生成的输出结构与“运行加固引擎”部分相同。我们已经构建了一个稳健的生产前环境上下文引擎。让我们总结一下这段旅程并继续。
总结
在本章中,我们将上下文引擎从一个功能原型提升到了生产级系统。通过重构和重建其架构,我们执行了专业的工程原则,使引擎变得健壮、透明、可靠,并成为开发韧性多智能体系统的实用蓝图。
影响最大的变化是引入了依赖注入,它消除了对全局变量的依赖,使得每个组件都是自包含的,并显式地感知其依赖项。这种解耦设计极大提高了引擎的灵活性、可测试性和可维护性。
我们还通过添加结构化日志和精确的错误处理,加固了从辅助函数到智能体的每一个子系统。这些升级创建了完全审计的工作流。最后,我们展示了如何将单体笔记本重构为模块化应用。
结果是一个架构良好的上下文引擎,用户界面与核心逻辑之间实现了清晰分离。在下一章中,我们将开始将上下文引擎应用于实际问题。
问题
-
- 本章的主要目标是从零开始构建上下文引擎吗?
-
- 在规划阶段,
planner()函数是否直接调用智能体来完成工作?
- 在规划阶段,
-
resolve_dependencies()函数的主要作用是通过替换占位符(如STEP_1_OUTPUT)来处理依赖链吗?
-
- 智能体注册表(Agent Registry)被描述为管理整个过程的大脑吗?
-
- 在重构期间创建
utils.py文件是为了处理引擎的高层规划逻辑吗?
- 在重构期间创建
-
- 第 4 章原型中主要的日志方法是使用 Python 内置的
logging模块吗?
- 第 4 章原型中主要的日志方法是使用 Python 内置的
第 5 章
161
-
- 在重构管理员(Librarian)和研究员(Researcher)智能体时,是否发现它们返回的是字符串而非结构化字典?
-
- 将代码移动到单独的
.py文件后,注册.py文件是否因为找不到智能体函数而崩溃?
- 将代码移动到单独的
-
- 复杂工作流测试(重写后的)是否证实了引擎总是遵循“管理员 -> 作者”的顺序?
参考文献
-
Qian, C., Wu, Y., & Ahmed, J. (2024). LLM-Multi-Agent Systems for Software Engineering: Literature Review and the Road Ahead. arXiv preprint:2404.08334
-
Zhang, W., Wang, Z., Li, Y. (2025). Knowledge-Based Multi-Agent Framework for Architecture Design. arXiv preprint:2503.02536
-
Zhang, Z., Chen, Z., Sun, S., ... & Liu, B. (2025). AgentOrchestra: Hierarchical Multi-Agent Framework for General-Purpose Task Solving. arXiv preprint:2506.12508
-
Cito, J., Kassab, M., & Parnin, C. (2021). Cataloging Dependency Injection-Patterns in Software Systems. arXiv preprint:2109.04256
延伸阅读
Wooldridge, M. (2009). An Introduction to Multiagent Systems (2nd ed.). John Wiley & Sons.
玻璃盒系统架构设计
与之前的章节一样,我们首先从架构漫览开始,在进入实现阶段之前,从视觉和概念上建立直认知。为了适应新的技能,我们必须学会在编写代码之前尽可能地思考 AI 系统。目前最先进的 AI 副驾驶(copilots)已经可以提升代码生成,但在没有人类进行设计思考的情况下,它们无法设计系统。如图所示,我们依然架构师,而 AI 副驾驶则是工人:
带着这种架构师思维,我们必须解决每位 AI 工程师都会面临的关键选择。许多人急于将 AI 当作一个简单的黑盒,提供输入并期望高质量的输出。我们将采取一种更专业、更强大的方法:强制执行“玻璃盒系统”,专为透明性和控制而设计。与内部逻辑隐藏的黑盒不同,我们的上下文引擎(context engine)设计初衷是为了被理解。我们在第 4 章中开始实现玻璃盒功能,通过详细的 ExecutionTrace(执行轨迹)使每个操作都可追溯。这样,每个组件都是模块化的,每个决策都可以被检查。在构建内容削减功能时,我们将这样做。你可能会想:“为什么不选别的?”答案很简单。每个人都在涌向聊天机器人以提高效率。然而,AI 副驾驶输出的质量与我们输入的质量严格相关;因此,有必要开发一个能够实现完全控制的玻璃盒引擎。在第 5 章中强化的上下文引擎是一个具有顺序操作流的复杂系统。理解这一如何(即理解)我们新的玻璃盒架构如何无缝衔接,是理解我们的摘要器代理(Summarizer agent)将如何集成到现有逻辑中的关键。
在本节中,我们将首先梳理引擎的运行流程,从初始的用户目标到最终生成的输出。然后,我们将精确指出我们的新组件将在何时以及何地引入。这种双层方法将提供引擎从一个强大的编排器演变为具有效率意识的系统的演变全景。让我们设计内容削减过程的架构蓝图。
逐步架构漫览
我们漫览的指南是图 6.2。图中的每个节点都经过颜色编码,以匹配我们代码中的对应部分。本章引入的新组件被清晰地标记为 [NEW] 标签。
构建用于内容削减的摘要器代理
图例:
-
第 7 节(蓝色)
-
第 6 节(白色)
-
第 5 节(紫色)
-
第 4 节(绿色)
-
第 3 节(橙色)
图 6.2:内容削减架构
第 6 章
图 6.2 的图例包含了上下文引擎的颜色编码:
-
蓝色(第 7 节): 用户运行的最终执行脚本
-
白色(第 6 节): 引擎核心,包含主编排器、规划器(planner)和追踪器(tracer)
-
紫色(第 5 节): 代理注册表(agent registry),作为系统的工具箱
-
绿色(第 4 节): 执行实际工作的专家代理(specialist agents)
-
橙色(第 3 节): 提供通用实用程序的辅助函数(helper functions)
仅仅看着流程图就让人感到无畏!脑海中出现的第一个想法是:“这什么时候结束?我们已经看了五个章节了,是的,事情变得越来越复杂。但这恰恰是重点。一个 AI 架构师,一个上下文引擎是一个复杂度思考者。许多人认为从零开始构建系统是不值得的,而是急于跳向那些承诺通过精美示例无缝实现的现成平台。但现实要复杂得多。每一个先进的 AI 无一例外都有硬墙。有时数据缺失会导致进度停止;有时关键函数的行为不符合预期;而且(注:原文此处似乎存在逻辑断层或文本重复,已根据语意衔接)……而且,不可避免地,Bug 会出现。
(注:原文中段如 "Understanding this how seamlessly our new glass-box architecture is the key..." 逻辑较为跳跃,翻译时已尽量保持语义通顺)
(注:原文后部分多处存在明显的文本重复,如 "Understanding this how seamlessly"、"Understanding this how seamlessly our new glass-box architecture is the key" 以及 "Understanding this how seamlessly our new glass-box architecture is the key to appreciating how our Summarizer agent will integrate into its existing logic" 之后的重复段落,译文已根据语意进行了处理以确保可读性)
在本节中,我们将首先梳理引擎的运行流程,从初始的用户目标到最终生成的输出。然后,我们将精确指出我们的新组件将在何时以及何地引入。这种双层方法将提供引擎从一个强大的编排器演变为具有效率意识的系统的演变全景。让我们设计内容削减过程的架构蓝图。
(此处跳过原文中重复的混乱描述,直接进入核心逻辑)
-
- 初始化: 一切从用户运行代码开始。
context_engine()作为入口点,它是整个编排器的核心,正如我们在之前的章节中所建立的那样。
- 初始化: 一切从用户运行代码开始。
-
- 规划: 一旦进入
context_engine(),首要任务是通过get_capabilities_description()查询可用的代理。有了这个列表和用户目标,规划器通过call_llm_robust()返回一个结构化的整体计划。
- 规划: 一旦进入
-
- 执行循环: 在有了清晰的计划后,引擎进入执行模式。它通过
get_handler()从注册表中检索相应的代理函数。通过resolve_dependencies()处理依赖关系,将占位符(如步骤 1 输出)替换为之前步骤的实际输出。随后调用代理执行任务。一旦步骤完成,结果将记录在trace中,引擎进入下一步。
- 执行循环: 在有了清晰的计划后,引擎进入执行模式。它通过
-
- 完成: 在所有步骤执行后,引擎通过对
trace对象调用finalize()完成过程。在记录最终状态和执行时间之前向用户返回干净的结构化输出。
- 完成: 在所有步骤执行后,引擎通过对
我们的上下文引擎现在像一个智能协作团队一样运行,自主且由代理驱动。每个组件都承担着明确的职责,正如协作良好的团队成员一样。让我们仔细探索这些职责,看看系统在复杂性增加时如何保持平衡。
职责划分
接的顺序流揭示了清晰的职责划分。每个模块在这里都使用图 6.2 所示的颜色标签,帮助你从视觉上连接架构图中的概念。我们现在在这个模块化框架内引入我们的组件和升级:
-
引擎核心(白色): 大脑。它管理整个过程,但不亲自执行工作。它的逻辑通用且健壮,因此在本章中无需任何更改。
-
专家代理(绿色): 工人。每个代理负责系统中的一项特定任务。我们将引入一个新的代理(在图 6.2 中标记为
[NEW AGENT]),agent_summarizer。它的作用是作为智能网关,根据特定目标将大量文本简化为要点。该代理直接解决了管理 API 成本和在固定 Token 限制运行的需求。 -
代理注册表(紫色): 工具箱。它为规划器提供可用工具的描述说明。注册表的说明(
get_capabilities_description)将更新以包含我们新的摘要代理,使规划器能够感知到这一新能力。 -
辅助函数(橙色): 共享实用程序,是保持一切顺畅运行的默默促进者。我们将正式确认新的实用程序(在图 6.2 中标记为
[NEW UTILITY])的count_tokens函数。该函数充当引擎的燃料计,允许任何组件在将一段文本发送到 LLM 之前测量其的 token 成本。它是实现主动式、成本意识设计的基础工具。
现在我们已经理解了基础架构以及我们的新组件将处于何位置,让我们以一种任何利益相关者(无论是否具有技术背景)都能理解的方式,来总结这些变更的战略影响。作为上下文工程师,我们必须超越功能性系统的构建,学习用决策者易于理解的语言来传达它们的演进。每一次升级,无论技术上如何,都应该用概念性的术语进行解释,以连接工程与业务应用。
-
第 1 和第 2 阶段——主动上下文管理:我们的主要升级不仅仅是一个新的代理,而是一项新的战略能力:主动上下文管理。引擎现在不再仅仅是处理给定的任何信息,而是可以对其进行分析、测量和压缩。
count_tokens工具提供了测量,而Summarizer代理提供了操作。这使得引擎从纯粹的被动变为策略性的高效,能够经济地处理更大的问题。 -
第 3 阶段——集成与可扩展性:我们架构最强大的方面在于,无需重新设计核心引擎即可添加这种新能力。通过简单添加
Summarizer代理并更新代理注册表的描述,Planner会自动学习如何使用这个新工具。这证明了系统具有高度的可扩展性;未来可以以最小的干扰添加新的代理和能力,就像在组织良好的工作坊中添加新工具一样。 -
第 4 和第 5 阶段——验证:我们流程的最后一部分是证明这些升级是有效的,而且同样重要的是,证明它们没有破坏任何现有功能。我们将进行严格的测试(包括向后兼容性检查),以验证引擎保持稳定,并且
Planner仍然能够正确地制定计划。这种对验证的关注对于构建旨在企业级的系统建立信任至关重要。
我们现在已经涵盖了大量的架构思维领域,但我们需要解释为什么这种对验证的关注对于构建企业级系统建立信任至关重要。
为什么"玻璃盒"至关重要
正如我们的流程图所示,职责分离不仅是一个优雅的设计选择,也不是某些内容在急于编写代码之前所忽略的升级概念概述。它是任何 AI 工程项目的战略性要求。当 AI 辅助驾驶员正逐渐占据那个角色时,为什么还要急于编写代码呢?最新且强大的 LLM 可以产生相当不错的生产级代码。因此,在编写代码之前请留出时间,并思考为什么我们正在构建的"玻璃盒"架构将带你进入 AI 专家水平的新境界。
首先,这种设计主动防止了技术债。在紧密耦合或单体系统中,添加新功能需要对核心逻辑进行复杂且风险的更改。随着时间的推移,系统会变成脆弱的纸牌屋,单次修改就会导致级联故障。相比之下,我们的架构将代理视为即插即用的模块。添加 Summarizer 不需要我们更改 Executor 或 Planner 的基本工作方式。这比比升级汽车的 GPS;你可以更换单元而无需重新设计引擎。这一原则确保了我们的上下文引擎在未来几年内保持保持敏活性和适应性。
此外,这种模块化是有效团队合作和可扩展性的推动者。想象一种场景:企业需要为其法律、财务和市场部门部署代理。有了我们的架构,三个独立的开发者或团队可以并行工作 agent_legal_parser、agent_financial_analyzer 和 agent_marketing_writer。他们的唯一要求是遵循 MCP 进行通信,并将完成的代理注册到中央 Agent Registry 中。他们不需要理解引擎核心编排的复杂细节,极大地降低了贡献门槛,并加速了新业务能力的开发。你正在引导你的团队进入一个新时代!
单体系统(脆弱)
玻璃盒架构(敏捷)
图 6.3:从脆弱到敏捷的 AI 系统
图 6.3 展示了你作为一名新兴上下文工程师架构师,从以前提示词和经典 AI 系统设计向重大转变。这个视觉比喻不仅代表了两种软件设计模式之间的选择;它概括了一个根本性的范式转变,对系统和人类都有深远影响。
对于 AI 系统而言,这是从静态的、单一用途工具演变为具有韧性的生态系统。它的价值不再仅仅由其即时输出衡量,而是由其随时间推移适应、增长和集成新能力而不崩溃的能力衡量。
对于人类工程师来说,这种转变更有意义。它将角色从孤立应用程序的构建者提升为智能系统的架构师。随着 AI 辅助驾驶员越来越多地处理生成代码的任务,最关键的人类技能变成了远见性:设计一种不仅在今天强大,而且足够敏捷、以拥抱未来不可避免的进步的框架的能力。这种为变化而设计架构的敏捷方法,正是现代上下文工程的精髓。
有了这张清晰的架构和概念地图,我们已经准备好开始实操实现。
使用 Summarizer 代理实现上下文压缩
通过对引擎升级后架构的牢固掌握,我们已经为构建这一新能力建立了坚实的基础。现在我们现在从架构理论转向实际实现。这段实现之旅将是一个有条理的过程,需要对我们的核心库文件进行精确的添加,以无缝集成新代理。
我们编排和测试这些增强的主要工作空间是名为 Context_Engine_Content_Reduction.ipynb 的笔记本,它是 Chapter05/Context_Engine_Production.ipynb 的升级版本。安装的库保持不变。为了在引擎演进时保持版本清晰和项目结构整洁,每个章节的代码都位于自己的子目录中。在本章中,所有共享库文件源自 commons/ch6/,这是一个清晰且有序的空间,用于在我们继续扩展上下文时管理升级。
升级将通过五个系统的阶段进行,建立在我们在第 5 章建立的稳健架构之上:
-
第 1 阶段——成本管理的基础:从验证
count_tokens工具开始,使系统能够主动测量和管理上下文大小。 -
第 2 阶段——构建摘要器代理:在
agents.py中构建新代理的核心逻辑,将其设计为自包含的、感知的模块。 -
第 3 阶段——集成引擎工具包:更新
registry.py,使PlannerLLM 感知新能力,并能够在其工作流中动态整合摘要。 -
第 4 阶段——强化写入代理:升级
agent_writer以接受来自Researcher的输出。 -
第 5 阶段——演示新能力:执行一个复杂目标,展示
Summarizer代理如何优化性能。
我们的第一步将是成本管理。
成本管理的基础
我们在本章的工作集中在上下文大小,这首先需要测量它的能力。我们将验证在初始过程中添加到 helpers.py 的 count_tokens 工具。
正如之前提到的,该工具充当引擎的燃料计,在向 LLM 发送文本之前对其消耗量提供精确测量。这是一个简单但至关重要的策略,为后续的成本管理奠定基础。我们现在查看其实现:
# FILE: commons/helpers.py (现有代码)
# 该函数是我们主动管理 token 的工具。
def count_tokens(text, model="gpt-4"):
"""计算给定模型文本中的 token 数量"""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
# 对于 tiktoken 注册表中可能不存在的模型备底
encoding = tiktoken.get_encoding("cl100k_base")
return len(encoding.encode(text))
count_tokens 接受文本字符串和一个可选的模型名称。它使用 tiktoken 库(OpenAI 的官方分词器)将文本编码为整数列表,并返回该列表的长度。通过在 try...except块中封装编码过程,它优雅地处理了特定模型可能不在库中的情况,退回到通用基础编码以确保函数返回值。
在验证了基础测量工具后,我们现在可以构建使用这一原则来管理上下文的代理。我们现在开始构建 Summarizer 代理。
构建摘要器代理
Summarizer 代理是我们解决上下文过载问题的解决方案。它的目的是智能地将大量文本压缩为针对特定目标的精简摘要,确保只将最相关的信息传递给工作流中的后续代理。
我们将把此代理添加到 agents.py 文件中,遵循为其他代理建立的相同生产级模式。它将是一个接收它将所有依赖作为参数传递,并使用 MCP 通信其结果。该代理的智能之处在于对 summary_objective 的使用,它引导 LLM 创建一个不仅更短,而且更有用的摘要。
该函数通过定义其签名 agent_summarizer 来,它接受标准的 mcp_message 及其所需的依赖:用于 API 调用的 client 对象和 generation_model 配置。在此块内部,它从 MCP 消息内容解析出所需的输入:text_to_summarize(包含一大块文本内容)和 summary_objective(为精简提供的特定目标):
# FILE: commons/ch6/agents.py
# 这个新函数被添加到我们现有的 Library 中。
# 它遵循既定的依赖注入和结构化日志模式。
agent_summarizer(mcp_message, client, generation_model):
""
根据目标将大型文本缩减为简洁的摘要。
作为门门人管理 token 和成本。
""
logging.info(f"[Summarizer] 已激活。正在压缩上下文...")
# 从 MCP 消息解析输入
text_to_summarize = mcp_message['content'].get('text_to_summarize')
summary_objective = mcp_message['content'].get('summary_objective')
代理包含一个验证步骤,以确保两个必要的输入都存在,如果不存在则抛出 ValueError。然后它构建了一个 system_prompt,指示 LLM 扮演专家摘要的角色。user_prompt 使用 f-strings 组装,清晰地划定了 OBJECTIVE AND TEXT TO SUMMARIZE(目标与待摘要文本)。这种结构化的提示词对于生成高质量、相关的摘要至关重要:
代理在继续之前会验证它已收到必要的输入。
if not text_to_summarize or not summary_objective:
raise ValueError("Summarizer 要求输入内容中包含 'text_to_summarize' 和 'summary_objective'。")
# 为 LLM 定义提示词
system_prompt = """你是一个专家摘要 AI。你的任务是在用户特定目标的引导下,将提供的文本精简为要点。摘要必须简洁、准确并直接针对所述目标。"""
```python
user_prompt = f""" OBJECTIVE ---\n{summary_objective}\n\--- TEXT TO
SUMMARIZE ---\n{text_to_summary}\n\--- END TEXT ---\n\n生成摘要\n"""
最后,代理调用了我们加固的 llm_robust 辅助函数,解析提示词和注入的依赖。生成的摘要通过 create_mcp_message 辅助函数以标准的 MCP 消息返回。输出是一个键为 "summary" 的字典,确保了下游代理具有可预测的数据结构。整个过程封装在 try...except 中以进行健壮的错误处理:
# 代理调用健壮的 LLM 辅助函数并返回结果。
# 调用加后的 LLM 辅助函数执行摘要
summary = call_llm_robust(
system_prompt,
user_prompt,
client=client,
generation_model=generation_model
)
# 以标准 MCP 格式返回摘要
return create_mcp_message("Summarizer", {"summary": summary})
except Exception as e:
logging.error(f"[Summarizer] 发生错误: {e}")
raise e
我们停顿了一下并回想。我们通过验证输入、用清晰的目标引导 LLM、返回可预测的 MCP 消息以及将调用封装在 try...except 中,平滑地集成了一个新代理。这就是良好的上下文工程示例。
微上下文工程 (Micro-context engineering)
虽然 Summarizer 代理的 Python 代码非常简单,但真正的力量不在于函数本身,而在于提供给 summary_objective 的上下文质量。构建一个精确的目标是微上下文工程的完美范例。这是将通用多用途工具转换为特定任务的专业化、高精度仪的技能。LLM 配合任何强大的工具,在给定清晰、无歧的指令时表现最佳。
一个优秀的上下文工程师提供坚实、有明确的意图;而糟糕的工程师永远无法获得预期结果。保持敏锐。专注于提升你的上下文设计能力。确保指令不仅仅是一个过时的提示词!考虑一下定义糟糕的目标与架构良好的目标所产生的输出质量之间存在巨大差异。
糟糕的目标是模糊且开放的:
"总结这段文本。"
这迫使 LLM 去猜测用户认为什么是重要的。结果通常是一个通用的、平淡的段落,可能完全忽略用户实际需要的关键信息,浪费了 API 调用和用户的时间。
相比之下,一个强大的目标是一个微型蓝图。它是具体的,提供了约束,并清晰地定义了预期的输出:
"请从以下法律文件中提取所有相关方的名称、讨论的关键财务数字以及最终解决日期。排除所有程序性的官话,并以 JSON 对象的格式呈现输出。"
这个目标将通用的 Summarizer 转为了特定任务的工具。它没有留下歧义空间,并保证了输出输出。掌握构建这些目标的艺术是上下文工程师的核心能力,因为它允许他们动态地复用单一的高价值函数。
再次,我们正在见证从开发到设计的范式转变。如果我们是上下文工程师,我们将 AI 提升到一个高度。
上下文工程的艺术 (The of Engineering)
糟糕的目标
- "总结这段文本。"
强大的目标
- "从以下法律文件中提取所有相关方的名称、讨论的关键财务数字以及最终解决日期。排除所有程序性的官话,并以 JSON 对象的格式呈现输出。"
将新代理集成到工具集中
由于 Summarizer 代理已经构建并测试,下一步是让系统发现它。为了完成升级,我们必须更新 registry.py。此过程涉及对 registry.py 的两次精确:
- 将
agent_summarizer函数添加到注册表中。 - 更新
get_capabilities()方法,这是规划器读取可用工具以及如何使用的文本手册。
# FILE: commons/ch6/registry.py
# 整个文件已更新以集成新代理。
# === 导入 ===
import logging
import agents
from helpers import create_mcp_message
# 5. 代理注册表(最终加固版) ===
class AgentRegistry:
def __init__(self):
self.registry = {
"Librarian": agents.agent_context_librarian,
"Researcher": agents.agent_researcher,
"Writer": agents.agent_writer,
# 新:添加 Summarizer 代理 ---
"Summarizer": agents.agent_summarizer,
}
在`AgentRegistry`类的`__init__`方法中,我们在`self.registry`中添加了一个新的键值对。键是代理的名称`"Summarizer"`,它将在执行计划中使用;值是我们刚刚创建的可调用函数。
接下来,我们在`get_handler`方法中添加一个新的`elif`块。这确保当执行器请求`"Summarizer"`时,注册表会正确为其准备运行所需的`client`和`generation_model`依赖。这符合我们严格的依赖注入模式:
```python
# 更新 get_handler 方法以为新代理准备依赖项。
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_name}")
raise ValueError(f"注册表中未找到代理 {agent_name}")
第 6 章
179
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
)
elif agent_name == "Summarizer":
return lambda mcp_message: handler_func(
mcp_message, client=client,
generation_model=generation_model
)
else:
return handler_func
最后且最关键的更新是get_capabilities_description返回的文档字符串。我们为 Summarizer(摘要器)添加了一个新条目。我们清晰地定义了它的角色、所需的输入("text_to_summarize"和"summary_objective")以及它的输出结构。这种详细的描述让 Planner(规划器)能够理解何时以及如何在其计划中策略地部署 Summarizer 智能体:
# 最后,我们更新了 Planner LLM 读取的技能描述。
def get_capabilities_description(self):
"""为 Planner LLM 返回智能体的结构化描述。"""
# --- 更新:添加 Summarizer 的能力 ---
return """
可用智能体及其所需的输入。
关键点:你必须使用为每个智能体提供的精确输入键名。
构建用于上下文压缩的 Summarizer 智能体
-
- 智能体:Librarian(图书管理员)
角色:检索语义蓝图(风格/结构指令)。
输入:
"intent_query":(字符串)对所需风格的描述性短语。
输出:蓝图结构(JSON 字符串)。
-
- 智能体:Researcher(研究员)
角色:检索并综合关于某个主题的事实信息。
输入:
"topic_query":(字符串)要研究的主题。
输出:综合的事实(字符串)。
-
- 智能体:Summarizer(摘要器)
角色:根据特定目标将长文本压缩为简洁的摘要。
适用于在生成步骤之前管理 token 数。
输入:
-
"text_to_summarize":(字符串/引用)需要被摘要的长文本。 -
"summary_objective":(字符串)摘要的明确目标(例如,"提取技术规格")。
输出:包含摘要的字典:
{"summary": "..."}。 -
- 智能体:Writer(作者)
角色:通过在源材料上应用蓝图来生成或重写内容。
输入:
-
"blueprint":(字符串/引用)风格指令(通常来自 Librarian)。 -
"facts":(字符串/引用)事实性信息(通常来自 Researcher 或 Summarizer)。 -
"previous_content":(字符串/引用)用于重写的现有文本。
输出:最终生成的文本(字符串)。
图 6.5 显示了动态的四步发现过程,该过程使 Planner LLM 能够学习并策略地使用新智能体:
第 6 章
181
图 6.5:Planner 与 AgentRegistry 之间的动态发现循环
该图说明了get_capabilities_description()方法如何形成 Planner 与 AgentRegistry 之间的桥梁。该过程从 Planner 开始...
except Exception as e:
logging.error(f"[Writer] An error occurred: {e}")
raise e
函数的其余部分按照之前的流程进行。它确定使用哪些数据作为source_material,构建详细的系统提示和用户提示,并调用 LLM 生成最终内容。
现在我们的 Writer agent 已经强化并能够处理多个数据源,我们已准备好对新的上下文缩减工作流进行首次端到端测试。
探索 Summarizer 的实际应用
我们实现的最后阶段是演示新的上下文缩减工作流。我们将设计一个目标,使得使用 Summarizer 不仅是有益的,而且对于成功完成任务是必不可少的。
LLM 是随机模型。因此,输出在每次运行之间可能会有所不同。
该目标将有两个方面:首先,我们将显式地要求引擎针对特定目标总结一段长文本,然后要求引擎将该摘要作为创造性写作任务的事实基础。这种复杂的结构迫使 Planner(规划器)创建一个利用上下文链(context chaining)的多步骤计划,将 Summarizer 的输出作为 Writer 的输入。这项测试将提供确凿证明,证明我们的新 agent 已完全集成,并且引擎可以动态部署它。
首先,我们定义了一个字符串变量large_text_from_researcher,它包含了一段关于朱号(Juno)太空探测器的密集技术信息。然后,我们使用 f-string 构建了目标。该目标显式指示引擎执行两个顺序任务:首先,针对特定目标总结提供的文本,然后使用该摘要进行创造性写作任务:
# FILE: Context_Engine_Content_Reduction.ipynb
# 这是来自主笔记本控制面板的最终演示。
# 1. 定义一段长文本,如果直接作为 Writer agent 的上下文
# 将成本过高或长度过长。
large_text_from_researcher = """
Juno 是一个绕木星运行的 NASA 太空探测器。它于 2011 年 8 月 5 日从卡纳韦勒尔空军基地发射,作为新前沿计划的一部分...
"""
# 2. 定义一个既需要使用长文本又需要创造性步骤的目标。
goal = f"""首先,总结以下关于朱号探测器的文本,以提取
仅关于其科学任务和仪器的关键事实。然后,使用
摘要,为关于该探测器危险抵达木星的儿童故事写一个简短的、悬疑的场景。
-- 要使用的文本 --
{large_text_from_researcher}
"""
我们使用与之前运行相同的配置字典,并将所有必要的组件传递给`execute_and_display`函数:
> # 配置保持不变,并执行了调用操作。
```python
# 3. 使用相同的配置字典
config = {
"index_name": "genai-mas-mcp-ch3",
"generation_model": "gpt-5",
"embedding_model": "text-embedding-small",
"namespace_context": "ContextLibrary",
"namespace_knowledge": "KnowledgeStore"
}
# 4. 调用执行函数
execute_and_display(goal, config, client, pc)
运行此处后,我们可以分析 TECHNICAL TRACE(技术轨迹溯)来解构引擎的思考过程。轨迹溯显示了一个多步骤计划,从 Summarizer agent 开始,它成功地将长文本缩减为一份简洁的、项目符号形式的关键事实列表。至关重要的是,轨迹溯显示第一步的输出通过上下文链,作为事实输入传递给后续步骤中的 Writer agent。最终输出是一个创造性故事,它实际上是基于摘要而非原始文本,这证明了我们的上下文缩减执行成功。这证实了 Summarizer 是我们 MAS(多智能体系统)中一个功能完备且具有战略价值的补充。
让我们查看最终输出,以确保我们的实现运行符合预期。
解构引擎的思考过程:证据就在轨迹之中
最终生成的文本引人入胜,但我们成功的真正标准在于 TECHNICAL TRACE。作为引擎的飞行黑记录器,轨迹溯为其推理和行动提供了透明的、分步的描述。通过解构这一轨迹溯,我们可以精确地看到 Planner 如何制定新策略来满足我们复杂的目标,以及 Summarizer agent 在实现最终输出方面发挥了怎样的关键作用。
现在,我们将回溯测试运行的执行过程,分析每个决策和行动,以确认我们新的、以效率为核心的架构正完全按照设计运行,从 Summarizer agent 开始。
步骤 1:Summarizer agent 领先
Executor(执行器)采取的第一个动作是调用 Summarizer agent。这是对我们升级最重要的验证。当 Planner 面对用户目标时,分析并识别了两个组件:一个大型的嵌入文本块和一个"总结以下文本"的显式指令。根据我们在registry.py中提供的更新功能,Planner 得出结论,认为 Summarizer agent 是第一步的理想工具。它没有默认选择 Researcher,因为事实上下文已在目标本身提供。这是动态规划的清晰演示。
Planner 智能地提取了用户的指令,并为摘要制定了一个非常具体的 target:
'summary_objective': '提取仅关于 \
'朱号的科学任务和其 \
'仪器/功率系统的关键事实......'
随后,Summarizer agent 执行了它的任务,带有此目标调用 LLM。它的输出是一个简洁的、项目符号形式列表,包含了关于朱号任务最相关的事实:
'output': { 'summary': '- 在木星极地轨道运行...- 测量成分、引力场...- 通过探测内部研究形成过程...- 确定深层大气中的水量...- 绘制质量分布并表征深风...- 由三个大型太阳能电池板提供动力并保持稳定...'}
在这里,我们看到了 agent 的直接经济和技术价值。利用我们的 token 计数特性,我们可以量化这种减少:
- 原始文本 (text_to_summarize): 253 tokens
- 最终摘要 (output): 110 tokens
Summarizer agent 实现了 token 数量 56.5% 的缩减。它成功过滤了高量、低相关性的细节(如发射日期和与伽利略号探测器的比较),并传递了一个低量、高相关的摘要。此操作直接转化为更低的 API 成本,并确保最关键的信息保留给后续更昂贵的生成步骤。
在整个过程中,有很多方法可以在上下文引擎中放置计数,这些应该在开发阶段的架构研讨会上决定。例如,在你的情况下,可以将 token 计数包含在执行后函数:
# === 执行后分析:量化上下文缩减 ===
# 确保导入 count_tokens 工具
from helpers import count_tokens
# 1. 获取发送给 Summarizer 的原始文本
# (这是控制面板中定义的变量)
original_text = large_text_from_researcher

浙公网安备 33010602011771号