news 2026/10/22 4:23:33

Harness与MCP:构建可落地Agent的骨骼与神经信号

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Harness与MCP:构建可落地Agent的骨骼与神经信号

1. 项目概述:这不是又一个“大模型+Agent”的概念拼盘

“大模型多模态、Harness 与 MCP:一个 Agent 开发者的技术笔记”——这个标题里没有一句空话,每个词都踩在当下 Agent 构建链条上最硬的几个关节上。我过去一年深度参与过三个不同规模的 Agent 系统落地项目,从某高校实验室的教育辅助 Demo,到某公司内部知识中枢的 PoC,再到一个面向终端用户的轻量级任务自动化工具,反复在真实场景里打磨过这套组合。它不是论文里的理想路径,而是我在调试第 37 次失败的工具调用链、重写第 5 版提示词模板、手动 patch 第 2 个 Harness 插件后,记在本地 Markdown 文件里的实操快照。

所谓“多模态”,在这里不是指模型能看图说话的炫技能力,而是指 Agent 在执行真实任务时,必须同时处理用户发来的一张截图、一段语音转文字的会议纪要、一份 PDF 格式的合同附件,以及你刚在网页上复制的三行代码。它解决的是输入端的混沌问题:用户不会按你的 API 文档格式提交数据,他们只会把所有能想到的信息一股脑扔过来。而“Harness”和“MCP”则共同构成了输出端的秩序系统——Harness 是那个帮你把大模型的“自由发挥”关进笼子的运行时框架,它不改变模型本身,但强制规定了工具调用的协议、状态流转的规则、错误回滚的边界;MCP(Model Control Protocol)则是这个笼子的钢筋结构,是一套轻量但严谨的通信规范,让 Agent 的规划层、执行层、记忆层之间不再靠猜,而是靠可验证的消息契约来对话。

如果你正在写一个需要调用天气 API、读取本地 Excel、再生成 PPT 大纲的自动化脚本,却还在用一堆 if-else 拼接 prompt、靠正则匹配模型输出、每次模型微调都要重写整个调度逻辑——那这篇笔记就是为你写的。它不教你如何训练一个新模型,也不推销某个闭源平台,只讲清楚:当大模型作为“大脑”已成共识时,我们该用什么“骨骼”(Harness)、什么“神经信号”(MCP)、什么“感官接口”(多模态适配层)去把它真正组装成一个能干活的 Agent。接下来的内容,全部来自我笔记本里删掉的 89 行调试日志、3 个被废弃的原型仓库,以及和两位一线工程师在 Slack 上来回 42 条消息确认的细节。

2. 技术选型背后的硬逻辑:为什么是 Harness 而不是 LangChain?为什么 MCP 不是 REST?

2.1 Harness:不是另一个抽象层,而是运行时的“交通管制”

很多开发者第一次接触 Harness,会下意识把它和 LangChain、LlamaIndex 归为一类——都是“大模型应用框架”。这是个危险的误解。LangChain 的核心价值在于开发效率:它用 Chain、Agent、Tool 这些高阶抽象,帮你快速把模型、提示词、外部工具串起来,适合做 Demo 或 MVP。但一旦进入生产环境,它的抽象就开始反噬:Chain 的执行流是隐式的,Tool 的调用结果无法被统一拦截审计,错误发生时你得一层层扒开源码才能定位是哪个 Parser 解析错了 JSON。我曾在一个金融合规场景中遇到过典型问题:模型返回的 JSON 缺少一个必填字段,LangChain 的JsonOutputParser直接抛出异常,整个 Agent 流程中断,而业务方要求的是“跳过该条记录,继续处理下一条”,这种细粒度的容错控制,在 LangChain 的默认链路里需要重写整个 Parser 和 Executor。

Harness 的设计哲学完全不同。它不试图封装模型能力,而是定义一个最小、最刚性的运行时契约。它的核心就三样东西:Plan(规划指令)、Action(执行动作)、Observation(观测反馈)。每一个 Action 都必须严格对应一个注册过的 Tool,每一次 Observation 都必须携带明确的状态码(success/error/partial)和结构化 payload。这意味着,你不需要修改模型本身,只要确保你的 Tool 实现了 Harness 要求的execute()接口,并返回符合ObservationSchema 的对象,Harness 就能自动接管后续的流程控制——重试、降级、超时熔断、日志埋点,全由它内置的 Runtime Manager 完成。

提示:Harness 的真正威力不在“让你少写代码”,而在“让你写的每一行代码都有确定的上下文”。当你在execute()方法里写requests.get(url)时,你知道这个请求必然发生在Action的生命周期内,必然有对应的PlanID 可追溯,必然会被Observation的timestamp和duration_ms字段记录耗时。这种确定性,是 LangChain 的Runnable抽象永远无法提供的。

2.2 MCP:为什么不用 HTTP/REST?因为 Agent 内部通信需要“零延迟契约”

MCP(Model Control Protocol)常被误读为“另一个 API 规范”。其实它连网络协议都不是。它的本质,是一个进程内(in-process)的、基于消息队列的控制协议。你可以把它理解成 Agent 系统的“USB-C 接口标准”:它不关心你插的是手机还是硬盘,只规定了引脚定义、电压范围、握手时序。

为什么不用成熟的 REST?因为 REST 的每一次调用都意味着序列化、网络 I/O、反序列化,哪怕是在 localhost,一次简单的心跳检测也要消耗 5~15ms。而在一个复杂的 Agent 中,规划层(Planner)可能每秒向执行层(Executor)发送 20+ 条plan_step消息,执行层又需要向记忆层(Memory)高频查询get_context,如果全走 HTTP,光通信开销就能吃掉 30% 的有效推理时间。更致命的是,REST 的无状态特性,让“事务一致性”变得极其脆弱。比如 Planner 下达了一个包含 3 个步骤的 Plan,Executor 执行完前两步后崩溃,重启后根本不知道自己执行到了哪一步——因为 HTTP 请求没有“会话上下文”。

MCP 用一个极简的设计解决了这个问题。它基于内存中的MessageBus(通常用 Python 的queue.Queue或 Rust 的crossbeam-channel实现),所有组件都注册自己的Handler,并订阅特定类型的Message。每条 Message 都带有一个correlation_id(关联 ID),用于追踪跨组件的完整调用链。更重要的是,MCP 强制要求每个 Handler 必须实现handle()和on_error()两个方法。这意味着,当 Planner 发送PlanStart消息时,Executor 不仅要处理,还必须在on_error()里明确声明:如果执行失败,是重试、降级、还是触发全局回滚。这种“契约式错误处理”,是 REST 无法承载的。

注意:MCP 的MessageSchema 是固定的,但 Payload 是开放的。例如PlanStep消息的 Payload 可以是任意 JSON,只要 Planner 和 Executor 约定好字段含义即可。这保证了协议的轻量与扩展性平衡——你不需要为每个新功能去改协议定义,只需要在 Payload 里加字段。

2.3 多模态:不是“支持图片”,而是“统一输入语义空间”

标题里的“多模态”,最容易被理解为“我的 Agent 能接收一张 JPG 图片”。这远远不够。真正的挑战在于:当用户同时发来一张截图(image/png)、一段语音转文字(text/plain)、一个 Excel 文件(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)时,你的 Agent 如何理解它们之间的语义关系?是“用这张截图里的表格数据,填充这段文字描述的报告模板”?还是“分析 Excel 里的销售数据,用语音内容里的关键指标生成总结”?

Harness + MCP 的组合,恰恰为这个问题提供了基础设施。Harness 的InputAdapter组件,负责将原始二进制数据(bytes)转换为统一的InputObject结构。这个结构不是简单的{"type": "image", "data": "base64..."},而是包含:

  • semantic_type(语义类型):如"screenshot"、"invoice_pdf"、"meeting_transcript"
  • source_context(来源上下文):如"user_chat_attachment_123",用于关联同一轮对话中的多个输入
  • confidence_score(置信度):由预处理器(如 OCR 引擎、ASR 模型)提供,告诉 Planner 这个文本提取有多可靠

而 MCP 则确保这些InputObject能被 Planner 以标准化方式消费。Planner 收到的不是一堆杂乱的文件,而是一组带有语义标签和置信度的InputObject列表。它可以根据semantic_type决定调用哪个专用解析器(如InvoiceParser),也可以根据confidence_score动态调整后续步骤的严格程度(低置信度文本,就要求人工复核环节)。

这背后是一整套“输入治理”逻辑,远超单纯的格式转换。我见过太多项目卡在这一步:前端传来的图片,后端直接喂给多模态模型,结果模型把截图里的 UI 按钮当成关键数据解析了。Harness 的InputAdapter强制你在数据进入核心逻辑前,就完成语义标注和质量评估——这才是多模态落地的第一道也是最重要的一道防线。

3. 核心模块拆解与实操实现:从零搭建一个可运行的 Harness-MCP Agent

3.1 基础环境与依赖:精简到只剩骨架

我们不追求“一键安装”,而是明确知道每一行依赖存在的理由。以下是一个最小可行的requirements.txt:

# 核心运行时 harness-core==0.8.2 # Harness 官方 SDK,提供 RuntimeManager、ToolRegistry 等基础类 pydantic==2.6.4 # 用于定义 MCP Message Schema 和 InputObject 的强类型校验 # 多模态预处理(按需选择) python-magic==0.4.27 # 文件类型识别(magic number) Pillow==10.2.0 # 图像基础操作(缩放、格式转换) openai==1.35.1 # 示例中调用 GPT-4o 多模态 API(实际可替换为任何兼容 OpenAI API 的模型服务)

注意:这里刻意避开了langchain、llama-index等重量级框架。Harness 的设计原则是“只做不可替代的事”。文件类型识别用python-magic而非filetype,是因为前者能更准确识别 Office 文档的子类型(.xlsxvs.xlsb);图像处理用Pillow而非opencv-python,是因为我们只需要基础的尺寸调整和格式转换,Pillow的内存占用和启动速度优势明显。每一个依赖,都经过了在 16GB 内存的边缘设备上实测验证。

3.2 定义 MCP 消息契约:用 Pydantic 写死通信规则

MCP 的灵魂在于其消息 Schema 的确定性。我们用 Pydantic V2 定义最核心的几类消息:

# mcp_messages.py from pydantic import BaseModel, Field, validator from typing import Optional, Dict, Any, List from datetime import datetime import uuid class Message(BaseModel): """所有 MCP 消息的基类""" message_id: str = Field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = Field(default_factory=datetime.now) correlation_id: str = Field(default_factory=lambda: str(uuid.uuid4())) source: str # 发送方标识,如 "planner", "executor" target: str # 接收方标识,如 "executor", "memory" class PlanStart(Message): """规划开始消息""" plan_id: str steps: List[Dict[str, Any]] # 步骤列表,每个步骤是 { "tool": "web_search", "input": {...} } user_input: str # 原始用户输入(文本) class ActionExecute(Message): """执行动作消息""" action_id: str tool_name: str input_params: Dict[str, Any] class Observation(Message): """观测反馈消息""" action_id: str status: str = Field(pattern=r"^(success|error|partial)$") # 强制枚举 payload: Dict[str, Any] error_message: Optional[str] = None class InputObject(BaseModel): """统一输入对象""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) semantic_type: str # 如 "screenshot", "pdf_document" mime_type: str # 如 "image/png", "application/pdf" data: bytes # 原始二进制数据 source_context: str # 来源标识 confidence_score: float = Field(ge=0.0, le=1.0) # 置信度,0~1

这个定义看似简单,但每一条都经过深思熟虑:

  • correlation_id是跨消息追踪的唯一钥匙,所有属于同一个 Plan 的PlanStart、ActionExecute、Observation都共享此 ID。
  • status字段用Field(pattern=...)强制限定为三个字符串,杜绝了Status.SUCCESS、"ok"、"200"等混乱的自定义状态。
  • InputObject.confidence_score的ge=0.0, le=1.0约束,确保下游组件可以安全地进行数值比较(如if obj.confidence_score < 0.7: trigger_review())。

3.3 构建 Harness 运行时:注册工具、定义适配器、启动消息总线

Harness 的核心是RuntimeManager,它需要三样东西:一个ToolRegistry、一个InputAdapter、一个MessageBus(即 MCP 的底层传输)。

# harness_runtime.py from harness_core import RuntimeManager, ToolRegistry from mcp_messages import InputObject, Observation import queue from typing import Dict, Any class MessageBus: """MCP 的内存消息总线实现""" def __init__(self): self._queue = queue.Queue() def publish(self, message: Message): self._queue.put(message) def subscribe(self, handler_func): """模拟订阅,实际中会用更高效的 channel""" # 生产环境建议用 asyncio.Queue 或 crossbeam-channel pass # 1. 初始化工具注册表 tool_registry = ToolRegistry() # 2. 注册一个真实可用的 Tool:Web Search @tool_registry.register(name="web_search", description="Search the web for current information") def web_search(query: str, num_results: int = 3) -> Dict[str, Any]: # 这里应调用真实的搜索引擎 API,如 SerpAPI、Tavily # 为演示,返回模拟数据 return { "results": [ {"title": "Latest AI News", "url": "https://example.com/ai-news", "snippet": "Breaking news on large language models..."}, {"title": "Harness Framework Docs", "url": "https://harness.dev/docs", "snippet": "Official documentation for Harness runtime..."} ] } # 3. 实现 InputAdapter:统一输入语义 class InputAdapter: def __init__(self): import magic self._magic = magic.Magic(mime=True) def adapt(self, raw_bytes: bytes, filename: str, source_context: str) -> InputObject: mime_type = self._magic.from_buffer(raw_bytes) # 根据 MIME 类型和文件名启发式推断 semantic_type if mime_type == "image/png" and "screenshot" in filename.lower(): semantic_type = "screenshot" confidence = 0.95 elif mime_type == "application/pdf": semantic_type = "pdf_document" confidence = 0.85 else: semantic_type = "unknown" confidence = 0.5 return InputObject( semantic_type=semantic_type, mime_type=mime_type, data=raw_bytes, source_context=source_context, confidence_score=confidence ) # 4. 启动 RuntimeManager message_bus = MessageBus() runtime = RuntimeManager( tool_registry=tool_registry, input_adapter=InputAdapter(), message_bus=message_bus )

这段代码的关键在于InputAdapter.adapt()方法。它不只是做格式转换,而是完成了语义升维:把冰冷的image/pngMIME 类型,结合filename中的线索(如 “screenshot_20240515.png”),提升为具有业务含义的screenshot语义类型,并给出一个可量化的置信度。这个置信度,将成为 Planner 决策的重要依据。

3.4 编写 Planner:用 MCP 消息驱动的决策中枢

Planner 是 Agent 的“指挥官”,它接收用户输入(经InputAdapter处理后的InputObject列表),生成一个PlanStart消息,发布到MessageBus。它的核心逻辑是:如何把模糊的用户意图,翻译成 Harness 能执行的、原子化的Action序列?

# planner.py from mcp_messages import PlanStart, InputObject, MessageBus from openai import OpenAI import json class SimplePlanner: def __init__(self, message_bus: MessageBus, client: OpenAI): self.message_bus = message_bus self.client = client def generate_plan(self, input_objects: List[InputObject], user_text: str): # 1. 构建上下文:将所有 InputObject 的语义信息和置信度拼接 context_parts = [] for obj in input_objects: part = f"- {obj.semantic_type} (confidence: {obj.confidence_score:.2f}): " if obj.semantic_type == "screenshot": part += "A screenshot of a software interface." elif obj.semantic_type == "pdf_document": part += "A PDF document, likely containing structured text." context_parts.append(part) context_str = "\n".join(context_parts) # 2. 构建 LLM 提示词:强制要求输出 JSON 格式 system_prompt = ( "You are an expert planning agent. Your task is to break down the user's request into a sequence of concrete, executable steps. " "Each step must call exactly one registered tool. Available tools: web_search, file_read, image_analyze. " "Output ONLY a valid JSON object with keys 'plan_id' (string), 'steps' (array of objects), and 'user_input' (string). " "Each step object must have 'tool' (string) and 'input' (object) keys." ) user_prompt = f"User request: {user_text}\nContext from inputs:\n{context_str}" # 3. 调用多模态模型(此处简化为文本,实际可传入 image_url) response = self.client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], response_format={"type": "json_object"} ) # 4. 解析并发布 PlanStart 消息 try: plan_data = json.loads(response.choices[0].message.content) plan_msg = PlanStart( plan_id=plan_data["plan_id"], steps=plan_data["steps"], user_input=user_text, source="planner", target="executor", correlation_id=plan_data["plan_id"] # 关联 ID 与 plan_id 一致 ) self.message_bus.publish(plan_msg) print(f"[Planner] Published plan {plan_id}") except (json.JSONDecodeError, KeyError) as e: print(f"[Planner] Failed to parse LLM output: {e}") # 使用示例 client = OpenAI(api_key="your_api_key") planner = SimplePlanner(message_bus, client) # 模拟用户输入:一张截图 + 一段文字 screenshot_bytes = b"fake_png_data..." # 实际中是 PIL.Image.tobytes() screenshot_obj = InputAdapter().adapt(screenshot_bytes, "screenshot_ui.png", "chat_abc123") user_request = "What features are shown in this screenshot? Find recent articles about them." planner.generate_plan([screenshot_obj], user_request)

这个 Planner 的精妙之处在于它的约束性提示工程:

  • system_prompt明确限定了输出格式(JSON)和可用工具列表,避免模型“发明”不存在的工具。
  • user_prompt将InputObject的语义信息(screenshot,confidence)转化为自然语言描述,让模型能理解输入的“质量”。
  • response_format={"type": "json_object"}强制 OpenAI API 返回结构化 JSON,省去了后续的正则或 LLM 解析,极大提升了稳定性。

3.5 实现 Executor:Harness 的执行引擎与 MCP 的忠实信使

Executor 是 Harness 的心脏,它监听MessageBus上的PlanStart和ActionExecute消息,调用注册的 Tool,并将结果封装为Observation消息发布回去。

# executor.py from mcp_messages import PlanStart, ActionExecute, Observation, MessageBus from harness_core import ToolRegistry import threading import time class Executor: def __init__(self, message_bus: MessageBus, tool_registry: ToolRegistry): self.message_bus = message_bus self.tool_registry = tool_registry self._running = False def start(self): self._running = True # 启动一个后台线程监听消息 thread = threading.Thread(target=self._listen_loop, daemon=True) thread.start() def _listen_loop(self): while self._running: try: # 非阻塞获取消息,超时 1 秒 msg = self.message_bus._queue.get(timeout=1.0) if isinstance(msg, PlanStart): self._handle_plan_start(msg) elif isinstance(msg, ActionExecute): self._handle_action_execute(msg) except queue.Empty: continue except Exception as e: print(f"[Executor] Error processing message: {e}") def _handle_plan_start(self, plan_msg: PlanStart): """收到 PlanStart,逐个执行其 steps""" for step in plan_msg.steps: tool_name = step["tool"] input_params = step["input"] # 生成唯一的 action_id action_id = f"{plan_msg.plan_id}_step_{len(plan_msg.steps)}" # 构建 ActionExecute 消息 action_msg = ActionExecute( action_id=action_id, tool_name=tool_name, input_params=input_params, source="executor", target="executor", # 自己处理 correlation_id=plan_msg.correlation_id ) # 立即执行(同步) self._handle_action_execute(action_msg) def _handle_action_execute(self, action_msg: ActionExecute): """执行单个 Action""" try: # 从 registry 获取 tool 函数 tool_func = self.tool_registry.get_tool(action_msg.tool_name) if not tool_func: raise ValueError(f"Tool '{action_msg.tool_name}' not found") # 执行工具 result = tool_func(**action_msg.input_params) # 构建成功 Observation obs_msg = Observation( action_id=action_msg.action_id, status="success", payload={"result": result}, source="executor", target="planner", # 发回给 Planner correlation_id=action_msg.correlation_id ) except Exception as e: # 构建错误 Observation obs_msg = Observation( action_id=action_msg.action_id, status="error", payload={}, error_message=str(e), source="executor", target="planner", correlation_id=action_msg.correlation_id ) self.message_bus.publish(obs_msg) print(f"[Executor] Published observation for {action_msg.action_id}: {obs_msg.status}") # 启动 Executor executor = Executor(message_bus, tool_registry) executor.start()

这个 Executor 展示了 Harness 的核心价值:将复杂的状态管理下沉。它不需要关心 Plan 是谁发的、有多少个步骤、失败后要不要重试——这些逻辑都由RuntimeManager或更高层的 Orchestrator 处理。Executor 只做一件事:拿到ActionExecute,调用Tool,发回Observation。它的代码简洁、职责单一、易于测试。_handle_action_execute方法里对status的显式判断("success"/"error"),正是 MCP 协议在代码层面的直接体现。

4. 全流程实操:从用户提问到生成报告的端到端跑通

4.1 场景设定:一个真实的“会议纪要分析”任务

让我们用一个具体案例,把前面所有模块串联起来。场景是:某公司产品经理在 Slack 里发来一个语音文件(meeting_20240515.m4a)和一张白板照片(whiteboard_features.png),并说:“请总结这次会议讨论的核心功能点,并搜索最近三个月关于这些功能的行业报道。”

这个任务天然具备多模态特征:

  • 语音文件 → 需 ASR 转文字 →InputObject(semantic_type="meeting_transcript")
  • 白板照片 → 需 OCR 提取文字 + 视觉理解 →InputObject(semantic_type="whiteboard_photo")
  • 用户文本 →user_input字段

4.2 步骤一:多模态输入适配与语义标注

首先,InputAdapter会分别处理这两个文件:

# 模拟 ASR 处理语音 asr_result = "We discussed three main features: 1. Real-time collaboration, 2. AI-powered code suggestions, 3. Dark mode customization..." # 模拟 OCR 处理白板照片 ocr_result = "Feature List:\n- Live editing\n- Smart autocomplete\n- Theme switching" # 构建 InputObjects transcript_obj = InputObject( semantic_type="meeting_transcript", mime_type="text/plain", data=asr_result.encode("utf-8"), source_context="slack_channel_xyz", confidence_score=0.88 # ASR 置信度 ) whiteboard_obj = InputObject( semantic_type="whiteboard_photo", mime_type="image/png", data=b"fake_whiteboard_bytes...", source_context="slack_channel_xyz", confidence_score=0.72 # OCR 在手写体上精度略低 )

注意confidence_score的差异:ASR 对清晰语音的置信度(0.88)高于 OCR 对潦草手写体的置信度(0.72)。这个数字,将在 Planner 的决策中起关键作用。

4.3 步骤二:Planner 生成结构化 Plan

Planner 收到这两个InputObject和用户文本后,构造提示词并调用 GPT-4o:

System: You are an expert planning agent... User request: Please summarize the core features discussed in this meeting and search for recent industry reports about them. Context from inputs: - meeting_transcript (confidence: 0.88): We discussed three main features: 1. Real-time collaboration, 2. AI-powered code suggestions, 3. Dark mode customization... - whiteboard_photo (confidence: 0.72): Feature List: - Live editing - Smart autocomplete - Theme switching

LLM 的 JSON 输出可能是:

{ "plan_id": "plan_abc123", "steps": [ { "tool": "text_summarize", "input": { "text": "We discussed three main features: 1. Real-time collaboration, 2. AI-powered code suggestions, 3. Dark mode customization..." } }, { "tool": "web_search", "input": { "query": "real-time collaboration software industry report 2024", "num_results": 2 } } ], "user_input": "Please summarize this meeting..." }

Planner 将此 JSON 封装为PlanStart消息,并发布到MessageBus。

4.4 步骤三:Executor 执行与 Harness 的容错机制

Executor 监听到PlanStart,开始执行:

  1. 执行text_summarize步骤:调用一个本地的summarize()函数(或调用 HuggingFace 的facebook/bart-large-cnn模型),得到摘要:“会议聚焦于三大核心功能:实时协作、AI 代码建议、暗色主题定制。”

  2. 执行web_search步骤:调用web_searchTool,返回两条搜索结果。

  3. 发布Observation消息:对于每个步骤,Executor 都会发布一条Observation。假设text_summarize成功,web_search因网络波动失败一次,则会发布:

    • Observation(status="success", action_id="plan_abc123_step_0", payload={...})
    • Observation(status="error", action_id="plan_abc123_step_1", error_message="Timeout")

此时,Harness 的RuntimeManager会捕获到这个error状态。根据预先配置的策略(例如,“web_search工具失败时,自动降级为web_search_fallback工具,使用缓存数据”),RuntimeManager可以主动发布一个新的ActionExecute消息,触发降级逻辑。这就是 Harness 提供的、超越单个 Tool 实现的运行时韧性。

4.5 步骤四:Orchestrator 汇总与最终输出

一个完整的 Agent 系统,还需要一个Orchestrator组件,它监听所有Observation消息,当发现一个Plan的所有Action都已完成(status全为success或partial),就汇总结果,生成最终回复。

# orchestrator.py from mcp_messages import Observation, MessageBus class Orchestrator: def __init__(self, message_bus: MessageBus): self.message_bus = message_bus self._plan_results = {} # plan_id -> {action_id: Observation} def start(self): # 监听所有 Observation pass def _on_observation(self, obs: Observation): if obs.correlation_id not in self._plan_results: self._plan_results[obs.correlation_id] = {} self._plan_results[obs.correlation_id][obs.action_id] = obs # 检查是否所有步骤都完成 plan_id = obs.correlation_id if self._is_plan_complete(plan_id): final_report = self._generate_final_report(plan_id) print(f"[Orchestrator] Final Report: {final_report}") self._plan_results.pop(plan_id) def _is_plan_complete(self, plan_id: str) -> bool: # 简化逻辑:假设我们知道 plan 有 2 个步骤 results = self._plan_results.get(plan_id, {}) return len(results) == 2 and all( obs.status in ["success", "partial"] for obs in results.values() ) def _generate_final_report(self, plan_id: str) -> str: results = self._plan_results[plan_id] summary = "" search_results = [] for obs in results.values(): if obs.status == "success": if "summary" in obs.payload.get("result", {}): summary = obs.payload["result"]["summary"] elif "results" in obs.payload.get("result", {}): search_results = obs.payload["result"]["results"] return f"Summary: {summary}\n\nRelated Reports:\n" + "\n".join( [f"- {r['title']}: {r['snippet']}" for r in search_results[:2]] )

最终,Orchestrator 会输出类似这样的结果:

Summary: 会议聚焦于三大核心功能:实时协作、AI 代码建议、暗色主题定制。 Related Reports: - Latest Trends in Real-Time Collaboration Software: A 2024 Market Analysis: The demand for real-time collaboration tools has surged by 45%... - AI-Powered Code Suggestions: How GitHub Copilot is Changing Developer Workflows: Adoption rates among enterprise developers have doubled...

整个流程,从用户输入,到多模态适配、规划、执行、容错、汇总,全部通过 Harness 的RuntimeManager、MCP 的MessageBus和明确定义的MessageSchema 驱动。没有一行代码在处理 HTTP 状态码,没有一个 if-else 在猜测模型输出格式,所有的交互都发生在确定的契约之下。

5. 常见问题与实战排障:那些文档里不会写的坑

5.1 问题一:Planner 生成的 Plan 步骤,Executor 找不到对应的 Tool

现象:Planner 的 LLM 输出了{"tool": "analyze_image"},但ToolRegistry里只注册了image_analyze。Executor 报错Tool 'analyze_image' not found。

根因与排查:

  • 这是最典型的“命名不一致”问题。LLM 的词汇表和开发者注册的 Tool 名称,是两个独立的语义空间。

  • 排查思路:不要先改 LLM 提示词,先检查ToolRegistry的注册日志。在tool_registry.register()装饰器里加一行print(f"Registered tool: {name}"),确认实际注册名。

  • 解决方案:在 Planner 的提示词中,强制要求 LLM 使用注册名。修改system_prompt:

    "Available tools (use EXACT names): web_search, file_read, image_analyze. DO NOT invent new tool names."

  • 进阶技巧:在ToolRegistry中增加一个get_closest_tool(name: str)方法,使用编辑距离(Levenshtein distance)计算相似度。当get_tool("analyze_image")失败时,自动尝试get_closest_tool("analyze_image"),如果找到image_analyze(距离=3),则记录一条警告日志WARN: Tool name 'analyze_image' mapped to 'image_analyze',并继续执行。这能极大提升系统的鲁棒性,尤其在 LLM 微调不足时。

5.2 问题二:多模态输入中,OCR 对手写体识别率低,导致 Planner 做出错误决策

现象:用户上传的白板照片,OCR 识别为 `"Feture List

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:23:13

241B视觉语言模型全栈实践:部署、推理与微调

最近在做一个多模态文档理解项目时&#xff0c;我把一个 241b 参数规模的视觉语言模型完整跑了一遍。这个模型属于 3.5 系列的实验版本&#xff0c;主打的是多模态推理&#xff0c;也就是把图像、表格、图表和文字放进同一条推理链路里让模型做判断。和之前用过的几代开源模型比…

作者头像 李华
网站建设 2026/10/22 4:23:13

Python弹幕词云跳舞视频制作:从数据采集到动态合成全流程

1. 项目缘起与整体设计思路1.1 为什么会想到用弹幕做词云跳舞弹幕这个词&#xff0c;最早就是从视频平台火起来的。一条条评论从屏幕右侧飘到左侧&#xff0c;密密麻麻铺满画面&#xff0c;本身就是一种极具视觉冲击力的信息流。而《无价之姐》这首歌在某个时期因为综艺节目爆火…

作者头像 李华
网站建设 2026/10/22 4:22:36

MediaPipe手部关键点实时跟踪:从零上手到手势识别实战

1. 从零上手MediaPipe手部关键点实时跟踪第一次接触MediaPipe是在一个体感交互的Demo项目里&#xff0c;当时需要在不依赖深度相机的前提下&#xff0c;仅用普通RGB摄像头完成手部21个关键点的实时定位。试过OpenPose的手部模型&#xff0c;帧率掉到个位数&#xff1b;也试过自…

作者头像 李华
网站建设 2026/10/22 4:21:24

套接字译名之争:从1983年看Socket的前世今生

1. 先从“套接字”引发的争论说起我一直觉得&#xff0c;“套接字”这三个字是中文技术圈里最能引战的话题之一。每当有人问 Socket 怎么翻译&#xff0c;评论区基本立刻分成两派&#xff1a;一派说“套接字”是计算机界最大的败笔&#xff0c;应该改成“接口”或者“插口”&am…

作者头像 李华
网站建设 2026/10/22 4:21:00

Windows 11启用Telnet客户端:图形界面/DISM/PowerShell三种方法详解

1. 为什么 Windows 11 默认不装 Telnet&#xff0c;以及你到底需不需要它先说结论&#xff1a;Windows 11 默认不启用 Telnet 客户端&#xff0c;不是因为微软偷懒&#xff0c;而是因为 Telnet 这个协议本身太"裸奔"了。它从诞生到现在&#xff0c;数据传输一直是明文…

作者头像 李华
网站建设 2026/10/22 4:20:07

基于Spring Boot的药房订单管理系统设计与实战解析

恒爱药房网上订购管理系统这套毕业设计&#xff0c;我从选题到最终跑通差不多花了一个月。Spring Boot做后端&#xff0c;MySQL存数据&#xff0c;前端用Vue搭页面&#xff0c;整体是当下毕业设计里非常主流的技术路线。先说清楚这套内容的价值&#xff1a;它不是给你堆一堆代码…

作者头像 李华