多智能体系统的上下文工程——构建上下文引擎

56 阅读40分钟

在前面的章节中,我们学习了如何自下而上地进行上下文工程,并构建智能体系统。但随着这些系统不断扩展,拥有更多智能体、更多职责、更多活动部件,它们会变得越来越难以管理。这正是 Context Engine(上下文引擎) 发挥作用的地方。

不要把 Context Engine 仅仅看作另一块软件组件,而应把它理解为一个结构化的编排层(orchestration layer),一种“思考型工作流(thinking workflow)”,它的作用是把一个模糊的想法——例如一个简单的用户请求——转化为一个完整成形、具备上下文感知能力的输出。你可以把它想象成智能体乐团的指挥。它本身并不直接执行任务;相反,它负责协调、指挥并连接各个专业角色,使它们能够完美协同工作。

本章中,我们将先从高层视角审视 Context Engine 的架构,然后深入讲解使其真正运转起来的各项函数。你会看到,从核心上讲,这个引擎遵循一个简单却强大的节奏:规划(plan)、执行(execute)、反思(reflect) 。到本章结束时,你不仅会理解 Context Engine 的工作原理,还会亲手构建一个属于你自己的上下文引擎。你将看到如何一步步组装它的组件,如何借助 MCP 将它连接到你的智能体,以及如何设计一个具备可扩展性、透明性与韧性的推理系统。

简而言之,本章将涵盖以下主题:

  • 设计 Context Engine 的架构
  • 组装系统
  • 运行引擎

下面我们先从定义 Context Engine 的架构开始。

设计 Context Engine 的架构

人工智能的发展已经从僵硬的、基于规则的系统,演进到灵活的生成式模型。你可以把早期 AI——通常被称为符号主义 AI(symbolic AI)——看作一本本精心手工编写的规则手册。它们在狭窄领域中表现令人印象深刻,但一旦面对歧义或超出脚本范围的情况,就会显得非常脆弱。现代 LLM 则颠倒了这一模式。它们展现出广泛的语言理解与通用推理能力,但缺乏具体、实时的知识,也无法稳定可靠地对外部系统采取行动。单靠 LLM 本身,很少能够持续交付业务价值。正是这一鸿沟促成了你在前几章中已经看到的两次转变。第一,RAG 用来自外部源的事实性上下文为 LLM 提供支撑,从而减少幻觉,并让回答具备情境感知能力。第二,AI 智能体为 LLM 提供工具和一定程度的自主性,使其能够执行多步骤任务。这两种思路共同推动模型更接近“可用的工作”。

然而,这种快速演化也带来了新的架构挑战。早期的 agentic 系统经常将智能体之间的交接关系硬编码,这在 demo 阶段看起来还不错,但一旦进入生产环境,很快就会纠缠成一团。如果你曾经把两个以上的智能体接在一起,你一定知道这种痛点。随着团队不断增加专用智能体,线性的“装配线”就无法再对自己的能力进行推理,也无法适应新的问题。因此,问题就变成了:我们如何从一组智能体,迈向一个自适应、自主的系统? 这正是 Context Engine 发挥作用的地方。

Context Engine 是一个元系统(meta-system) ,一个围绕高层目标来组织专用智能体的智能控制器。它不只是一个单一函数;它是一个结构化工作流,负责把一个模糊意图转化为一个有依据、具备上下文感知能力的输出。虽然“Context Engine”这个名称是本框架特有的,但这种模式在先进的生成式 AI 平台中已被广泛采用。像 ChatGPT 或 Gemini 这样的系统,并不是孤立存在的单一模型。它们本质上是带有控制器的应用,这些控制器负责管理会话、编排工具并维护上下文。从概念上说,这些控制器扮演的角色与 Context Engine 是相同的。

这种架构的独特之处,在于它明确地将规划(planning)执行(execution) 分离开来,而这一点受到了智能体推理前沿研究的启发。Context Engine 并不是采用一个简单循环,而是采用一个双阶段过程:

首先,Planner(规划器) 充当战略核心。它围绕用户目标进行推理,并将 Agent Registry(智能体注册表) 作为一套可用能力的“工具箱”,以便从中选择最佳的智能体或函数。因此,本章中我们将构建新的 Planner 和 Agent Registry 工具。

随后,Planner 使用 LLM 生成一个针对具体任务量身定制的、动态的逐步执行计划。计划准备完成后,会被交给 Executor(执行器) ——也就是那个负责运营调度的管理者,由它按照正确的顺序调用各个专用智能体。

正如第 3 章所介绍的那样,我们还将利用双重 RAG(dual RAG) ——静态知识数据与动态指令——来增强 Context Engine 的灵活性。

在本节中,我们将设计 Context Engine。我们会先通过一张架构图进行直观讲解,然后再拆解它所依赖的各个组件。整体概览请见图 4.1。

image.png

图 4.1:Context Engine 的架构

架构概览

如图所示,Context Engine 是一个动态的、多阶段工作流。图中每种颜色都代表一个不同的功能层,它们协同配合,将一个高层目标转化为一个完整生成的输出。

我们从最外层开始,在那里,整个过程以用户为起点,也以用户为终点。橙色组件表示这两个触点:用户的高层目标(User’s High-Level Goal)最终输出(Final Output) 。所谓目标,是一种广义指令,例如“写一个悬疑故事”。它只是一个简单请求,并没有预先定义好的结构。而最终输出则是一个完整、精修后的结果,用来满足这个目标。

一切的中心,是以蓝色表示的 Context Engine 本体。它是整个系统的编排者,是那个并不直接执行任务、但用战略智能管理整个工作流的大脑。在它内部有三个关键模块:

  • Planner:战略核心,负责接收用户目标并设计逐步执行计划。
  • Executor:执行管理者,负责落实计划,调用专用智能体,并管理它们之间的数据流。
  • Tracer:透明记录器,负责记录每一个动作,以便调试和洞察。

这个工作流的第一阶段是战略规划(strategic planning) ,在这一阶段中,Planner 会与黄色和红色组件协作。它首先咨询黄色高亮显示的 Agent Registry,也就是那套列出所有可用智能体及其能力的工具箱。获取这些信息后,它会联系外部红色高亮服务——LLM(例如 GPT-5) 。Planner 会将用户目标和工具箱描述一并发送给 LLM,然后由 LLM 返回一个结构化的 JSON 计划。这个计划在图中以虚线箭头表示,随后流回引擎内部,并交由 Executor 接手。

第二阶段是执行(execution) ,在这一阶段中,Executor 会协调绿色的 Specialist Agents(专用智能体) 和外部红色服务。它会按照计划逐步调用以下角色:

  • Librarian(馆员) :负责执行程序性 RAG(procedural RAG),提取风格蓝图。
  • Researcher(研究员) :负责执行事实性 RAG(factual RAG),收集并综合相关信息。
  • Writer(写作者) :负责将风格与事实结合,产出最终内容。

这些智能体中的每一个都与外部服务进行交互。Librarian 和 Researcher 会查询 Vector DB(向量数据库) ,而 Researcher 和 Writer 则可能调用 LLM 来精炼事实或生成文本。

这里的一项关键创新是上下文链式传递(context chaining) ——这是第 1 章中介绍过的内容,在图中用循环的绿色箭头表示。该机制确保整个工作流是有状态的:一个智能体的输出(例如 Librarian 提供的蓝图)会成为下一个智能体(例如 Writer)的输入。

有了这个总体概览之后,我们现在已经准备好深入探索那些真正赋予 Context Engine 生命力的具体函数了。

功能深潜

接下来,让我们看看 notebook 中的各个函数是如何把图 4.1 中的架构真正“跑起来”的。核心辅助函数负责以下基础任务:

  • call_llm_robust():LLM 接口,供 Planner 和各智能体与 OpenAI API 通信。
  • get_embedding():将文本转换为向量,用于 Pinecone 向量存储。它既用于存储数据,也用于将请求嵌入后去查询 Pinecone。
  • query_pinecone():允许智能体查询 Pinecone 向量存储。
  • create_mcp_message():Executor 与智能体交互时使用的标准化通信协议。
  • resolve_dependencies():上下文链式传递的核心,负责将智能体输入接好线,并在上下文链中使用前一步任务的输出作为后续任务的输入。

我们还需要一些专用智能体(绿色组件):

  • agent_context_librarian():获取动态语义指令蓝图。
  • agent_researcher():获取静态数据并综合事实。
  • agent_writer():生成最终内容。

随后,系统还需要编排与工具箱函数(蓝色与黄色组件):

  • AgentRegistry:初始化智能体注册表。
  • get_handler():为 Executor 获取执行特定任务所需的智能体函数。
  • get_capabilities_description():向 Planner 提供智能体列表。
  • planner():驱动 Planner,编排 Agent Registry 与 LLM 的交互以生成计划。
  • context_engine():主编排函数,按顺序运行 Planner 与 Executor。
  • ExecutionTrace:实现 Tracer,记录每一个步骤以便调试。

这些函数与架构图中的箭头关系一一对应:

  • 用户请求 → 调用 context_engine()

  • 在引擎内部 → 首先调用 planner()。它会使用 AgentRegistry.get_capabilities_description()call_llm_robust() 来生成计划。

  • Executor 遍历计划,并使用 resolve_dependencies() 为当前智能体组装上下文。

  • Executor 使用 AgentRegistry.get_handler() 获取正确的智能体(例如 agent_researcher())并调用它。

  • 各智能体会调用辅助函数:

    • Librarian 和 Researcher → query_pinecone()(其内部会用到 get_embedding()
    • Researcher 和 Writer → call_llm_robust()

每个智能体都会用 create_mcp_message() 返回一个结果,Executor 会将其保存起来,供下一步使用。这个循环会持续下去,直到整个计划完全执行完毕并返回最终输出。至此,我们就走完了整个架构。现在,让我们开始真正搭建这个系统!

组装系统

我们已经花了一些时间去理解 Context Engine 的结构,以及每个组件如何为更大的工作流作出贡献。在本节中,我们将从“理解”转向“构建”。接下来,我们的关注点将从架构蓝图转向真正运行起来的系统本身。Context_Engine.ipynb notebook 将成为我们的工作空间,在这里,我们会一块一块地组装这个引擎,把那些齿轮对接起来,把电路连接起来,让图中的设计真正运转起来。

我们将按照一个合乎逻辑的顺序,自下而上地构建这个引擎。一个强大的编排器,只有在它所领导的团队也足够强大时才真正有效。因此,我们的第一项任务是创建专用智能体(specialist agents) ,也就是系统的功能核心。等到这些智能体准备就绪,我们再开发它们的工具箱(toolkit) ——也就是 Agent Registry,它能让这些能力变得可发现且易于管理。最后,我们会组装 Planner 与 Executor,也就是那套把所有部件整合成统一运作体的协调型智能。我们先从专用智能体开始!

关于延迟的说明: 本书和配套代码仓库中构建的 Context Engine 执行的是复杂的、多步骤推理,而不是简单的一次性回答。你在 Colab 中观察到的延迟,本质上是“思考时间”——因为引擎正在动态规划并执行一系列 API 调用(例如先规划,再做 RAG,再进行生成)。这也是为什么像 Gemini 或 ChatGPT 这样的先进平台,在处理复杂请求时也需要一个“思考片刻”的原因。

专用智能体

在我们能够真正让 Context Engine 全面运转起来之前,我们必须先构建它的一线专家——那些真正执行工作的智能体。正如我们已经说明的,这个多智能体系统由三个专用智能体组成:Librarian、Researcher 和 Writer。每个智能体都承担着不同的职责,而所有智能体都通过 MCP 进行通信。MCP 在这里扮演的是一种通用语言,或者说是一种标准化的“信息集装箱”,用于承载系统中流动的信息。如果没有这样一个共享标准,每个智能体都需要单独定制集成逻辑,那样会让系统变得脆弱且难以扩展。MCP 解决了这个问题,因为它确保每一份数据——无论是 Librarian 给出的风格蓝图,还是 Researcher 给出的事实摘要——都被封装成完全相同的格式。

而正是这种标准化,使得上下文链式传递(context chaining) 成为可能。每个智能体接收一个 MCP 消息作为输入,并返回另一个 MCP 消息作为输出,这使 Executor 可以直接把数据传下去,而不必担心翻译或格式转换的问题。这种无缝的信息流,正是有状态多步骤推理得以实现的基础,也是整个引擎运作的根基。

Context Librarian 智能体

我们的第一个智能体是 Context Librarian,其实现函数为 agent_context_librarian。它的职责是识别用户意图,并从向量存储中取回合适的语义蓝图(semantic blueprint)。正如前面所解释的,这个蓝图会告诉 Writer 应该如何组织和设计内容风格。

Librarian 首先会从 MCP 消息中提取一个 intent_query,例如 “suspenseful narrative blueprint(悬疑叙事蓝图)”。这正是第一个关键的解释时刻:一个宽泛的创意构想,被转化为了机器可读的表达。它要从高层请求中解读出期望的情绪或格式,把“悬疑”这样的宽泛概念翻译成一个精确、可执行的检索查询,用于访问它的知识库:

def agent_context_librarian(mcp_message):
    """
    Retrieves the appropriate Semantic Blueprint from the Context Library.
    """
    print("\n[Librarian] Activated. Analyzing intent...")
    requested_intent = mcp_message['content'].get('intent_query')

    if not requested_intent:
        raise ValueError("Librarian requires 'intent_query' in the input content.")

接着,Librarian 使用 query_pinecone() 在上下文向量存储中执行语义检索。你可以把这个向量存储理解为一个“配方库”,而不是一个事实数据库。每一个蓝图都描述了某一类写作应当如何组织和呈现。这里的检索并不仅仅是匹配单词,而是在匹配意义,因此即便措辞不完全相同,系统也能找到概念上最接近的条目:

results = query_pinecone(requested_intent, NAMESPACE_CONTEXT, top_k=1)

如果找到蓝图,它就会被封装成一个新的 MCP 消息;如果没有找到,Librarian 仍然会返回一个有效消息,只是其中包含的是通用指令:

if results:
    match = results[0]
    print(f"[Librarian] Found blueprint '{match['id']}' (Score: {match['score']:.2f})")
    blueprint_json = match['metadata']['blueprint_json']
    content = blueprint_json
else:
    print("[Librarian] No specific blueprint found. Returning default.")
    content = json.dumps({"instruction": "Generate the content neutrally."})

return create_mcp_message("Librarian", content)

这最后一步体现了系统的韧性。理想情况下,系统会找到一个高度匹配、由专家精心设计的蓝图,并将其传递给下游的 Writer,从而确保输出高度定制化;但如果没有匹配到相关蓝图,系统也不会失败,而是优雅地退回到中性指令,确保工作流始终能够执行完成,只不过结果会更通用一些。

Researcher 智能体

接下来登场的是 Researcher,也就是系统中的“调查记者”。如果说 Librarian 负责结构与语气,那么 Researcher 负责将一切建立在事实之上。它的任务是查询知识向量存储,提取与某个主题最相关的信息,并将其综合成一段简洁的事实摘要。

让我们来看一看,这个智能体是如何把一个宽泛主题转化为一份聚焦且可验证的简报的。这个过程始于它从传入的 MCP 消息中收到自己的任务——topic_query。它的第一步动作,是在 NAMESPACE_KNOWLEDGE 向量存储中执行语义检索,取回 top_k=3 个最相关的文本片段:

def agent_researcher(mcp_message):
    """
    Retrieves and synthesizes factual information from the Knowledge Base.
    """
    print("\n[Researcher] Activated. Investigating topic...")
    topic = mcp_message['content'].get('topic_query')

    if not topic:
        raise ValueError("Researcher requires 'topic_query' in the input content.")

    results = query_pinecone(topic, NAMESPACE_KNOWLEDGE, top_k=3)

    if not results:
        print("[Researcher] No relevant information found.")
        return create_mcp_message("Researcher", "No data found on the topic.")

    print(f"[Researcher] Found {len(results)} relevant chunks. Synthesizing...")
    source_texts = [match['metadata']['text'] for match in results]

这是一个关键的设计选择:通过检索多个来源,Researcher 能够获得比单一结果更全面、更细腻的信息集合。

不过,这个智能体最重要的工作发生在综合(synthesis) 阶段。它不会简单地把检索到的原始文本直接传下去。相反,它会借助一个精心设计的提示词,指示 LLM 扮演一个专家级研究综合 AI(expert research synthesis AI)

system_prompt 设置了严格的护栏,要求 LLM 严格聚焦于已提供的事实,不得添加外部信息

system_prompt = """You are an expert research synthesis AI.
Synthesize the provided source texts into a concise, bullet-pointed summary relevant to the user's topic. Focus strictly on the facts provided in the sources. Do not add outside information."""

这是减轻幻觉、确保最终摘要真正立足于检索数据的重要一步。与此同时,user_prompt 会把检索到的文本块组装成清晰、有结构的格式,供 LLM 处理:

user_prompt = f"Topic: {topic}\n\nSources:\n" + "\n\n---\n\n".join(source_texts)

findings = call_llm_robust(system_prompt, user_prompt)

return create_mcp_message("Researcher", findings)

最终综合出的 findings,构成了一份新的知识产物:它被提炼过、足够简洁,并且与最初主题直接相关。这个输出会以 MCP 消息形式返回,供下游智能体继续使用。这样的“两步法”——先检索,再综合——确保了 Context Engine 的最终输出既有良好结构(Librarian 的功劳),又具备事实可靠性。

Writer 智能体

Writer 智能体是我们这条装配线中的最后一个专家;它是那位“大工匠”,负责将 Librarian 提供的蓝图与 Researcher 提供的研究结果组合起来,生成最终文本。它承担着最终综合的职责:把抽象风格与原始事实编织成一篇打磨完成、前后连贯的内容。它还可以处理重写任务,把之前生成的内容作为输入,这让整个引擎具备了对自己工作进行迭代优化的能力。

让我们来看看,这个智能体是如何巧妙组装输入并产出最终结果的。首先,它从 MCP 消息中收集自己的原材料:来自 Librarian 的蓝图、来自 Researcher 的综合事实,或者来自之前写作步骤的 previous_content

def agent_writer(mcp_message):
    """
    Combines the factual research with the semantic blueprint to generate the final output.
    Crucially enhanced to handle either raw facts OR previous content for rewriting tasks.
    """
    print("\n[Writer] Activated. Applying blueprint to source material...")

    blueprint_json_string = mcp_message['content'].get('blueprint')
    facts = mcp_message['content'].get('facts')
    previous_content = mcp_message['content'].get('previous_content')

接下来是一个关键检查:如果没有定义文本应该如何写的蓝图,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 变得很灵活。它可以在不同模式之间切换(新生成或重写),而无需改变其核心设计。在更大的工作流中,这种灵活性意味着同一个智能体可以被复用于多个优化阶段。

在选择好正确输入之后,Writer 会构造两个提示词:一个 system prompt 用来定义“怎么写”,一个 user prompt 用来定义“写什么”:

# The Writer's System Prompt incorporates the dynamically retrieved blueprint
system_prompt = f"""You are an expert content generation AI.
Your task is to generate content based on the provided SOURCE MATERIAL.
Crucially, you MUST structure, style, and constrain your output according to the rules defined in the SEMANTIC BLUEPRINT provided below.

--- SEMANTIC BLUEPRINT (JSON) ---
{blueprint_json_string}
--- END SEMANTIC BLUEPRINT ---

Adhere strictly to the blueprint's instructions, style guides, and goals. The blueprint defines HOW you write; the source material defines WHAT you write about.
"""

user_prompt = f"""
--- SOURCE MATERIAL ({source_label}) ---
{source_material}
--- END SOURCE MATERIAL ---

Generate the content now, following the blueprint precisely.
"""

这种提示词的分离,正是 Writer 真正智能性的体现所在。system prompt 提供的是创作框架,也就是来自蓝图的风格和结构约束;而 user prompt 提供的是内容实质,也就是 LLM 必须据以创作的事实或既有文本。

通过将 how(怎么写)what(写什么) 分开,Writer 就为模型提供了高度清晰的指令。LLM 能够非常明确地理解:它究竟要做什么,以及在什么约束条件下去做。因此,输出既能保持风格一致,也能保持事实有据可依。

最后,Writer 将这些提示发送给 LLM,获取结果,并把它封装成标准化的 MCP 消息,供后续步骤使用:

final_output = call_llm_robust(system_prompt, user_prompt)
return create_mcp_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") 这样的接口,而这个方法会返回准确的可调用函数。这个设计非常干净:没有硬编码,没有执行中途的 import,也没有脆弱的条件判断。只是一个简单的查表过程,却能精确地告诉 Executor 应当执行哪个函数:

def get_handler(self, agent_name):
                """Retrieves the function associated with an agent name."""
    handler = self.registry.get(agent_name)
    if not handler:
        raise ValueError(f"Agent '{agent_name}' not found in registry.")
    return handler

最后,也是最重要的方法,并不是为了代码本身,而是为了 LLMget_capabilities_description() 会返回一段结构化、对人类可读的描述,列出每个智能体的职责、输入和输出。这段内容是提供给 Planner 中 LLM 的“上下文”。当 Planner 调用 LLM 来生成计划时,会把这段文本一并带上,好让模型知道有哪些工具可用、每个工具期望什么输入、又会产出什么输出:

def get_capabilities_description(self):
"""
Returns a structured description of the agents for the Planner LLM.
This is crucial for the Planner to understand how to use the agents.
    """

    return """
Available Agents and their required inputs:
1. AGENT: Librarian
   ROLE: Retrieves Semantic Blueprints (style/structure instructions).
     INPUTS:
     - "intent_query": (String) A descriptive phrase of the desired style or format.
     OUTPUT: The blueprint structure (JSON string).

2. AGENT: Researcher
   ROLE: Retrieves and synthesizes factual information on a topic.
     INPUTS:
     - "topic_query": (String) The subject matter to research.
     OUTPUT: Synthesized facts (String).

3. AGENT: Writer
     ROLE: Generates or rewrites content by applying a Blueprint to source material.
     INPUTS:
- "blueprint": (String/Reference) The style instructions (usually from Librarian).
- "facts": (String/Reference) Factual information (usually from Researcher). Use this for new content generation.
- "previous_content": (String/Reference) Existing text (usually from a prior Writer step). Use this for rewriting/adapting content.
   OUTPUT: The final generated text (String).
""": Final generated text
    """

这段描述写得是否清晰,直接决定了 Planner 的推理质量。模糊或不完整的描述,会导致计划失效;清晰、详细的描述,则能赋予 Planner 设计出逻辑严密、可执行工作流的能力。

最后,我们实例化注册表本身。只需要这一行代码,就像给整个工作坊的目录系统“通电”一样,把系统对所有智能体的认知一次性激活起来:

AGENT_TOOLKIT = AgentRegistry()

有了这个结构,Planner 就能够对自己的团队进行智能推理:它清楚谁负责什么、谁启动时需要什么,以及它们的输出如何彼此衔接。

Context Engine

我们已经认识了这些专家,也已经在 Agent Registry 中把他们整理归档。下一步,是让整支团队像一个整体那样去思考和行动。这正是 Context Engine 的职责所在。

从核心上说,这个引擎运行的是一个简单却强大的循环,它反映了人类处理复杂问题时的方式:规划、执行、反思。Planner 从战略层面进行思考,把一个宽泛目标拆解成一组清晰步骤。Executor 则真正“下场干活”,执行这份计划,在合适的时刻调用合适的智能体,并随着上下文演化把信息持续传递下去。Execution Tracer 则扮演一个安静但关键的观察者角色,它记录下每一步动作,使我们之后不仅能看到系统做了什么,还能理解它为什么这么做。

接下来的各节中,我们会先从 Planner 开始,再讲 Executor,然后加入 Tracer。最后,我们会在 context_engine() 中把它们连接起来,并走一遍完整示例。

Planner

我们先从 planner 函数开始,它是整个引擎的战略核心。它的角色有点像一个专家级项目经理:把一个模糊、高层、面向人的目标,翻译成一份精确的、逐步展开的、机器可读的 JSON 执行计划。它的智能性,来自于它能够把 LLM 当作推理伙伴来使用。

Planner 需要两样东西:用户目标,以及来自 Agent Registry 的能力描述。有了它们之后,它就开始分析:

def planner(goal, capabilities):
  """
    Analyzes the goal and generates a structured Execution Plan using the LLM.
    """

    print("[Engine: Planner] Analyzing goal and generating execution plan...")

这里,goal 是高层请求;capabilities 则是我们通过 AgentRegistry.get_capabilities_description() 返回的、对人类可读的智能体目录,描述了各智能体的输入与输出。

接着,Planner 为 LLM 准备一份结构化简报。这个提示同时完成两件事:一是列出可用工具,二是设定规划规则。你可以把这理解为 Planner 在告诉模型:“这里是你可以使用的专家,这里是你该如何引用先前结果,以及这里是你必须输出的格式”:

system_prompt = f"""
You are the strategic core of the Context Engine. Analyze the user's high-level goal and create a structured Execution Plan using the available agents.

--- AVAILABLE CAPABILITIES ---
{capabilities}
--- END CAPABILITIES ---

INSTRUCTIONS:
1. The plan MUST be a JSON list of objects, where each object is a "step".
2. You MUST use Context Chaining. If a step requires input from a previous step, reference it using the syntax $$STEP_X_OUTPUT$$.
3. Be strategic. Break down complex goals (like sequential rewriting) into distinct steps. Use the correct input keys ('facts' vs 'previous_content') for the Writer agent.

EXAMPLE GOAL: "Write a suspenseful story about Apollo 11."
EXAMPLE PLAN (JSON LIST):
[
    {{"step": 1, "agent": "Librarian", "input": {{"intent_query": "suspenseful narrative blueprint"}}}},
    {{"step": 2, "agent": "Researcher", "input": {{"topic_query": "Apollo 11 landing details"}}}},
    {{"step": 3, "agent": "Writer", "input": {{"blueprint": "$$STEP_1_OUTPUT$$", "facts": "$$STEP_2_OUTPUT$$"}}}}
]

EXAMPLE GOAL: "Write a technical report on Juno, then rewrite it casually."
EXAMPLE PLAN (JSON LIST):
[
    {{"step": 1, "agent": "Librarian", "input": {{"intent_query": "technical report structure"}}}},
    {{"step": 2, "agent": "Researcher", "input": {{"topic_query": "Juno mission technology"}}}},
    {{"step": 3, "agent": "Writer", "input": {{"blueprint": "$$STEP_1_OUTPUT$$", "facts": "$$STEP_2_OUTPUT$$"}}}},
    {{"step": 4, "agent": "Librarian", "input": {{"intent_query": "casual summary style"}}}},
    {{"step": 5, "agent": "Writer", "input": {{"blueprint": "$$STEP_4_OUTPUT$$", "previous_content": "$$STEP_3_OUTPUT$$"}}}}
]

准备好这份简报之后,Planner 就调用 LLM,把它当作推理伙伴。整个过程被包裹在稳健的 try...except 结构中,用于处理潜在错误,比如 LLM 返回格式错误的 JSON。调用 LLM、解析响应之后,Planner 还会执行一个关键校验:它会检查输出是否是一个合法的列表。为增强韧性,它还会处理另一种情况:LLM 可能把列表包在字典中返回(例如 {"plan": [...]}),这时它会把列表提取出来。如果结构仍然不正确,或者发生了其他错误,except 分支就会记录详细错误信息,包括原始 LLM 输出,以便调试,然后抛出异常、终止引擎。只有在成功解析并通过校验之后,计划才会被返回给 Executor 使用:

plan_json = ""
try:
    plan_json = call_llm_robust(system_prompt, goal, json_mode=True)
    plan = json.loads(plan_json)

    # Validate the output structure
    if not isinstance(plan, list):
        # Handle cases where the LLM wraps the list in a dictionary (e.g., {"plan": [...]})
        if isinstance(plan, dict) and "plan" in plan and isinstance(
            plan["plan"], list
        ):
            plan = plan["plan"]
        else:
            raise ValueError("Planner did not return a valid JSON list structure.")

    print("[Engine: Planner] Plan generated successfully.")
    return plan
except Exception as e:
    print(f"[Engine: Planner] Failed to generate a valid plan. Error: {e}. Raw LLM Output: {plan_json}")
    raise e

这种设计之所以有效,是因为它给了 LLM 一份明确的“契约”去遵循。接下来,我们就会看到,Executor 是如何把这份计划真正转化为行动的。

Executor

Executor 就像系统里的现场工头:它执行每一个步骤,调用正确的智能体,并且最重要的是,把上下文不断向前推进,让后续步骤能够建立在前面结果之上。

在调用某个智能体之前,Executor 会先检查输入中是否存在占位符(例如 $$...$$ 这样的内容)。这些标记相当于对先前结果的引用,告诉系统:“这里要用第 1 步的输出。”通过解析这些占位符,Executor 就能把一份静态计划转化成一个真正连贯的工作流。这正是上下文链式传递在实际中的体现。完成这一过程的,是辅助函数 resolve_dependencies()。它会扫描智能体所需输入中的所有 $$...$$ 占位符。

函数的外层部分定义了 resolve_dependencies,并对输入参数做了一个深拷贝。这确保了原始计划结构(之后可能还会用到)不会在占位符替换过程中被意外修改(mutate):

def resolve_dependencies(input_params, state):
    """
    Helper function to replace $$REF$$ placeholders with actual data from the execution state.
    This implements Context Chaining.
    """
    # Use copy.deepcopy to ensure the original plan structure is not modified
    resolved_input = copy.deepcopy(input_params)

内部的解析器(基本情况)定义了嵌套函数 resolve。它的“基本情况(base case)”是处理单个字符串值。它会检查这个字符串是否是一个占位符(即以 $$ 开头并以 $$ 结尾)。如果是,它就会去 state 字典中查找对应的引用键(例如 STEP_1_OUTPUT),并返回真实数据:

# Recursive function to handle potential nested structures
def resolve(value):
    if isinstance(value, str) and value.startswith("$$") and value.endswith("$$"):
        ref_key = value[2:-2]
        if ref_key in state:
        # Retrieve the actual data (string) from the previous step's output
            print(f"[Engine: Executor] Resolved dependency {ref_key}.")
            return state[ref_key]
        else:
            raise ValueError(f"Dependency Error: Reference {ref_key} not found in execution state.")

内部解析器的“递归情况”则负责处理更复杂的嵌套结构。如果当前值不是字符串占位符,它会继续检查它是不是字典或列表。如果是,就递归地对其中的每个元素继续调用 resolve。这使得函数能够识别并替换深层嵌套在输入中的占位符。对于既不是占位符、也不是嵌套结构的内容,则直接原样返回。

最后,外层函数通过调用 resolve(resolved_input) 来执行整个解析过程:

    elif isinstance(value, dict):
        return {k: resolve(v) for k, v in value.items()}
    elif isinstance(value, list):
        return [resolve(v) for v in value]
    return value

return resolve(resolved_input)

正如你所看到的,Executor 就像一个总机接线员,会在当前执行状态(也就是引擎的短期记忆)中查找 ref_key,并动态地用前一步骤的真实输出替换占位符。正是这个看似简单的函数,让复杂而有状态的工作流成为可能。

Execution Tracer

有了 Planner 与 Executor 的协同,Context Engine 已经具备了功能性,但它还不够透明。为了补上这个闭环,我们需要一种方式来观察系统内部发生的事情,追踪每一个决策与动作是如何展开的。因此,ExecutionTrace 类就像是 Context Engine 的“飞行记录仪”,静静地记录从初始目标到最终输出的整个推理过程。下面我们更仔细地看一下。

当一个新任务开始时,Tracer 会用用户目标进行初始化。它会设置几个占位变量:plan 用来记录 Planner 的输出,steps 用来记录 Executor 将要采取的动作,status 用来记录执行进度。同时,计时器也会在这里启动,以便系统稍后计算整个工作流花了多长时间:

class ExecutionTrace:
    """Logs the entire execution flow for debugging and analysis."""
    def __init__(self, goal):
        self.goal = goal
        self.plan = None
        self.steps = []
        self.status = "Initialized"
        self.final_output = None
        self.start_time = time.time()

一旦 Planner 制定出策略,Tracer 就会通过 log_plan() 把它记录下来。这个方法会存储完整的 JSON 计划,让我们能够保留一份“系统原本打算怎么做”的记录:

  def log_plan(self, plan):
    self.plan = plan

随着 Executor 执行计划,log_step() 会把每一步都记录下来。对于每一个步骤,它都会捕获:调用了哪个智能体、原始计划输入是什么(planned_input)、解析后的上下文是什么(resolved_context),以及最终产出了什么(output)。这就形成了一份清晰、按时间顺序排列的执行轨迹:

def log_step(
    self, step_num, agent, planned_input, mcp_output, resolved_input
):
"""Logs the details of a single execution step."""

    self.steps.append({
        "step": step_num,
        "agent": agent,
        "planned_input": planned_input,
        "resolved_context": resolved_input,
        "output": mcp_output['content']
    })

当整个过程完成后,Tracer 会通过 finalize() 做收尾工作。它会记录最终状态(例如 SuccessFailed),保存最终输出,并计算从开始到结束的总耗时:

def finalize(self, status, final_output=None):
    self.status = status
    self.final_output = final_output
    self.duration = time.time() - self.start_time

这些方法合在一起,使 Tracer 成为了引擎中一位一丝不苟的“书记官”。无论我们是在调试失败、分析性能,还是只是想研究工作流是如何展开的,它都能帮助我们在任何时间点重建系统的推理过程。从本质上说,Execution Tracer 赋予了 Context Engine 每一个自主系统都需要的东西:可见性、可追责性,以及一种安静但可靠的保证——我们始终能够看见它是如何思考的。

把一切连接起来

现在,我们已经具备了所有构件:会思考的 Planner、会行动的 Executor,以及记录每一个动作的 Tracer。最后一步,就是把它们接进同一个控制循环中。

context_engine() 函数就是整个系统的“点火开关”。它通过编排完整的双阶段过程,让一切真正运转起来:先规划工作,再执行计划。下面我们一步一步来看。

当一个新任务启动时,这个函数会打印目标,并创建一个 execution Tracer 实例,后者会立即开始记录。同时,它还会加载 Agent Registry,也就是那本告诉引擎“系统里有哪些智能体以及如何调用它们”的目录。从这一刻开始,每一个动作、决策和输出都会被记入 trace 中。

def context_engine(goal):
 """
    The main entry point for the Context Engine. Manages Planning and Execution.
    """

    print(f"\n=== [Context Engine] Starting New Task ===\nGoal: {goal}\n")
    trace = ExecutionTrace(goal)
    registry = AGENT_TOOLKIT

接下来进入规划阶段(phase 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 the trace even in failure for debugging
        return None, trace

在这里,引擎接收用户目标并开始制定计划。它先从 registry 中获取一份完整的智能体能力描述,然后交给 Planner,后者会借助 LLM 生成一份 JSON 行动计划。这份计划会把目标拆解成多个有序步骤,明确说明哪个智能体该做什么,以及按什么顺序做。Planner 一旦完成,Tracer 就会立刻把这份计划记录下来,从而在真正执行之前,先保留系统的“策略意图”。如果这一步出错——例如格式错误或规划失败——系统也会干净地记录失败信息,并保留 trace 后退出。

有了计划之后,引擎就进入执行阶段(phase 2)

print(f"\n[Engine: Executor] Starting Step {step_num}: {agent_name}")

    try:
        handler = registry.get_handler(agent_name)

        # Context Assembly: Resolve dependencies
        resolved_input = resolve_dependencies(planned_input, state)

        # Execute Agent via MCP
        # Create an MCP message with the RESOLVED input for the agent
        mcp_resolved_input = create_mcp_message(
            "Engine", resolved_input)
        mcp_output = handler(mcp_resolved_input)

        # Update State and Log Trace
        output_data = mcp_output["content"]

        # Store the output data (the context itself)
        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 {step_num}({agent_name}):{e}"
        print(f"[Engine: Executor] ERROR: {error_message}")
        trace.finalize(f"Failed at Step {step_num}")
        # Return the trace for debugging the failure
        return None, trace

这个循环就是 Context Engine 真正开始“干活”的地方。Executor 会遍历计划中的每一步,并调用对应的智能体。在调用之前,它会先使用 resolve_dependencies() 检查输入中是否存在占位符(例如引用前面步骤输出的内容),并用当前执行状态中保存的真实数据将其替换掉。然后,这些输入会被封装成一个 MCP 消息并发送给对应智能体,后者的响应就成为该步骤的输出。每个输出都会被保存到 state 字典中,形成一套短期记忆,供后续步骤读取。与此同时,Tracer 会记录下沿途的一切细节:调用了哪个智能体、传入了什么输入、上下文是如何被解析的、又产出了什么结果。

当所有步骤都执行完成后,引擎会进入收尾阶段:

final_output = state.get(f"STEP_{len(plan)}_OUTPUT")
trace.finalize("Success", final_output)
print("\n=== [Context Engine] Task Complete ===")
return final_output, trace

此时,最后一步的输出会被取出并记录为最终结果。Tracer 会将本次运行标记为成功,记录最终结果,并计算从开始到结束的总耗时。最终,这个函数会返回两样东西:生成出的内容,以及完整的执行轨迹。

运行引擎

我们已经把这套拼图的每一块都组装好了:专用智能体、注册表、Planner、Executor 和 Tracer。在完成了所有这些架构工作之后,现在是时候拧动钥匙,听听引擎是如何运转起来的了。下面我们来看,当 Context Engine 被要求从头到尾完成一个创意任务时,会发生什么。我们先从一个简单提示开始,让它写一个关于阿波罗 11 号的、面向儿童的悬疑故事片段:

print("******** Example 1: STANDARD WORKFLOW (Suspenseful Narrative) **********\n")
goal_1 = "Write a short, suspenseful scene for a children's story about the Apollo 11 moon landing, highlighting the danger."

# Run the Context Engine
# Ensure the Pinecone index is populated (from Ch3 notebook) for this to work.
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")
    # Optional: Display the trace to see the engine's process
    # trace_1.display_trace()

当这个目标被传入引擎时,Planner 会立即开始工作:

  • 它推理得出:如果要创建一个既“悬疑”又突出“危险”的场景,首先需要一个风格指导。因此,它计划的第一步是调用 Librarian,获取一个悬疑风格蓝图。
  • 接着,它识别到其中的事实性部分——“阿波罗 11 号登月”——并知道自己需要准确细节。因此,第二步是让 Researcher 去提取相关事实。
  • 最后,既然风格与内容的规划都具备了,它就会创建第三步:调用 Writer,用第 1 步得到的蓝图与第 2 步得到的事实结合起来,生成最终内容。

然后,Executor 会按这份逻辑计划去执行:

  • ExecutionTrace 类会记录计划、每个步骤、输入、输出和最终结果。
  • planner 函数会根据用户目标与智能体能力生成 JSON 执行计划。
  • Executor 会执行该计划,解析依赖,并调用智能体。
  • resolve_dependencies() 辅助函数会用前一步的真实输出替换占位符。

这份输出正好提供了我们所期待的 Context Engine 的完整可追溯性。它首先显示任务标题和目标:

******** Example 1: STANDARD WORKFLOW (Suspenseful Narrative) **********
=== [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.

接下来,系统会向管理员和用户展示整个任务流的 trace。这在企业级战略项目中如今已经是一项基本要求,在世界某些地区甚至已经成为法律约束(例如《欧盟 AI 法案》)。无论如何,对于一个战略决策过程来说,能够解释系统是如何得出结果的,都是最佳实践,而这正是我们在这里所做的:

[Engine: Planner] Analyzing goal and generating execution plan...
[Engine: Planner] Plan generated successfully.

[Engine: Executor] Starting Step 1: Librarian

[Librarian] Activated. Analyzing intent...
[Librarian] Found blueprint 'blueprint_suspense_narrative' (Score: 0.66)
[Engine: Executor] Step 1 completed.

[Engine: Executor] Starting Step 2: Researcher

[Researcher] Activated. Investigating topic...
[Researcher] Found 2 relevant chunks. Synthesizing...
[Engine: Executor] Step 2 completed.

[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.

=== [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.
Numbers creep. The guidance drifts. The margin thins. Fuel is low.
I take manual control of the Lunar Module Eagle. The stick is firm. My glove is steady. The engine’s tone is a thin, living wire.
The cabin is small. A soft hiss in my ears. Breathing. Fans. A tremor underfoot. Shadows crawl over the ground and hide the truth of it.
We descend. Slow. Slower. The surface slides by, gray and strange. The target shifts. I nudge us. Left. Forward. Hold.
The world listens. Broadcast live. A billion quiet rooms. A billion held breaths.
The hum rises, falls. The Eagle steadies. The scene tightens to a pin.
Then—stillness.
Silence folds in. My heartbeat steps out of time and back again. The Moon waits, wide and wordless.
I look down at that pale ground. Distance, then inches. I move carefully. Deliberate. Every motion a decision.
The hatch. The edge. The black beyond the light. I feel the weight change. Not heavy. Not free. Just different.
A boot hovers. I watch the shadow first. It kisses the surface before I do.
Then contact. A first step. On the Moon.
The world is there with me, far away and everywhere at once. The air in my suit whispers. The surface holds. The moment stretches thin, like a note, and does not break.

==================================================

在生产环境中,你可以决定只向管理员展示执行 trace,或者把输出保存到执行日志中。你也可以决定在训练阶段让用户也能看到执行 trace,以便他们理解 Context Engine 的工作方式。这些都可以在落地实施时,通过 workshop 来共同决定。

随着这次成功执行的完成,我们现在可以退后一步,审视一下自己究竟构建出了什么。架构中的每一个组件都在协同工作,把抽象意图转化为结构化、可追踪、且有意义的输出。接下来,让我们回顾一下这次架构跃迁,以及那些使其在实践中如此优雅运作的设计原则。

总结

本章要解决的核心挑战,是预定义工作流的僵化性。我们的目标,是构建一个先进的控制器,使其能够通过对手头工具的能力进行推理,动态地规划并执行复杂任务。整个旅程从构建 Planner 开始——它充当战略核心,负责创建一份逐步达成目标的“配方”。然后我们构建了 Executor,也就是负责落实计划的操作管理者。最后,我们加入了 Tracer,以确保整个过程具备透明性并可调试。

本章引入的关键创新,是规划阶段。在这个阶段中,Planner 会查询 Agent Registry——一个描述每个专用智能体(Librarian、Researcher、Writer)能力的工具箱。掌握了这些信息之后,Planner 会利用外部 LLM 生成一份针对用户具体高层目标量身定制的、多步骤 JSON 计划。这将 做什么(what to do)如何做(how to do it) 分离开来。随后,Executor 按照这份 JSON 计划,以正确顺序调用各专用智能体。在这一阶段中,系统落实了第 1 章中介绍过的上下文链式传递(context chaining) 。这使得系统能够处理复杂、彼此依赖的任务,而这正是线性工作流做不到的。

本章最终构建出了一个功能完整的 Context Engine,它已经具备某种“为自己思考”的能力。由于能够动态生成自己的工作流,这个系统不再只是一个简单的流水线,而是一个智能化、模块化的工作坊。在下一章中,我们将继续强化这个引擎,使其面向真实世界应用:集成生产级特性,例如上下文管理、安全护栏与错误处理。

问题

  • Executor 会创建用于实现用户目标的战略性、逐步执行计划吗?(是或否)
  • Agent Registry 的主要目的是记录每一个动作以便调试吗?(是或否)
  • 文中是否将 context chaining 描述为“一个智能体的输出成为下一个智能体输入”的过程?(是或否)
  • 架构图中的专用智能体(Librarian、Researcher 和 Writer)是否用蓝色表示?(是或否)
  • resolve_dependencies() 函数是否允许智能体查询向量数据库?(是或否)
  • Planner 是否使用 Agent Registry 中的 get_capabilities_description() 方法来理解有哪些工具可用?(是或否)
  • context_engine() 函数是否是与外部 LLM 直接交互的主要接口?(是或否)
  • Writer 智能体是否负责执行 procedural RAG 来获取风格蓝图?(是或否)
  • 整个过程是否由橙色组件首尾包裹,分别表示用户目标与最终输出?(是或否)
  • 所有智能体是否都通过 Model Context Protocol(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. arXiv preprint arXiv:2201.11903.

Yao, S., Zhao, J., Yu, D., Du, N., Sha, H., & Tsvetkov, Y. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv preprint arXiv:2210.03629.

Schick, T., Dwivedi-Yu, J., Dessì, R., Raileanu, R., Lomeli, M., Zettlemoyer, L., Cancedda, 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. arXiv preprint arXiv:2308.08155.