ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent工程化实战:LangGraph、MCP与Harness安全架构

AI Agent工程化实战:LangGraph、MCP与Harness安全架构 如果你以为做一个 AI Agent就是把模型 API 封装成一个while循环让模型一遍遍调工具那你会发现demo 能跑项目上不了线。这个判断不是唱反调而是很多人在真正开始做 Agent 之后才意识到的一件事——模型只是大脑Agent 是一套完整的工程系统。大脑负责想工程负责让它安全地动。这套工程系统由三块拼图组成LangGraph 负责编排流程MCP 负责接入工具Harness 负责圈定边界。这篇文章就来把这三块拆开从安全架构到 Harness再到 LangGraph 和 MCP 的实战最后聊聊底层源码到底应该怎么看。1. 先看本质Agent 不是模型能力而是工程控制力1.1 为什么“一个 while 循环调用模型”走不远最简单的 Agent 雏形很多人写过拿到用户问题拼进 prompt调一次模型模型说“我需要查一下天气”于是你解析它输出的工具调用参数执行函数把结果拼回去再调一次模型。看起来没毛病循环个三五次任务完成了。但你只要把它放进真实场景问题立刻冒出来模型返回的工具调用格式有一点点偏差你解析就崩了工具执行抛异常没人知道该重试还是该跳过某个步骤陷入死循环token 费用在悄悄燃烧工具返回了包含恶意指令的网页内容模型被提示词注入牵着走用户按了一次 CtrlC整个状态没了下次又得从头开始。这些问题没有一个是“换个更强的模型”能解决的。它们全部属于工程问题。这就是为什么现在讨论 AI Agent 时大家越来越强调 harness执行框架/控制壳、编排层、工具协议和安全边界而不是单纯比谁的 prompt 写得好。1.2 Agent 的真正组成模型 编排 工具协议 Harness如果要把一个 Agent 拆成最小组成大概是四层层次作用常见实现模型层负责理解、决策、生成各类大模型 APIOpenAI 兼容接口等编排层决定 Agent 下一步做什么调模型、调工具、还是结束LangGraph、自研状态机工具协议层让 Agent 以统一方式调用外部工具和数据MCP、function callingHarness 层包住整个循环负责安全、审计、超时、上下文管理Codex Harness 类工程、自研执行沙箱很多人做 Agent只关注第一层和第二层选个好模型画个流程。真正让一个 Agent 能上线、能长期跑、能被团队维护的是第三层和第四层。工具协议解决“怎么让模型稳定地操作外部世界”Harness 解决“外部世界能不能信任这个模型、模型出错了系统怎么兜底”。1.3 一条主线把模型的“自由发挥”关进可控流程里这篇文章所有内容可以用一句话串起来AI Agent 的本质是把模型的自由发挥关进一个可控、可观测、有边界的工程流程里。LangGraph 负责提供“流程图纸”MCP 负责提供“标准接口”Harness 负责提供“安全护栏和安全员”。三者缺一个Agent 要么跑不稳要么不敢跑。2. Harness 是 Agent 的安全架构不是可有可无的壳2.1 Harness 到底管什么先给一个直观理解。Harness 直译是“马具/挽具”在 Agent 工程里它指的是包裹在模型和工具之外的执行控制环境。模型本身不是直接跑在你电脑上、直接调用你的文件系统和网络的它只能通过 harness 提供的接口行动。这意味着 harness 决定了模型“能做什么”和“能做到什么程度”。一个典型的 Agent harness 要管下面这些事控制循环模型 → 工具 → 模型 → 工具……何时停止。上下文管理塞给模型的系统提示、工具描述、历史消息如何组织如何防止上下文无限膨胀。安全边界模型能访问哪些文件、哪些网络、哪些命令。审计日志每一步模型说了什么、调了什么工具、传了什么参数、返回了什么结果全部可回溯。异常处理工具超时、模型返回非法格式、循环次数超标、费用达到上限都要有明确动作。如果你只把它想成“一个循环”那这些事确实都可以忽略。但一旦 Agent 要操作真实系统忽略每一项都可能变成事故。2.2 安全架构的四道边界从工程实践看Agent 的安全架构至少要有四道边界第一道权限边界。Agent 进程应该以最小权限运行。需要读文件就只给需要读的目录需要写文件就只给一个临时目录需要执行命令就先问自己一句“真的需要让模型直接执行 shell 吗”。绝大多数 demo 翻车都是因为让模型拿了管理员权限去跑命令。第二道资源边界。必须限制最大迭代次数、单次工具调用超时、总 token 消耗、并发数。否则一个循环 bug就能把账号余额烧掉一大截。这里有一个经验值先设置一个明显偏小的上限跑通流程比如最多 5 步、单步超时 10 秒稳定后再逐步放开。第三道信息边界。网络请求返回的内容、工具输出的文本都不能无条件当作“可信指令”。网页可能包含提示词注入日志文件里可能藏着让模型输出密钥的诱导语句。对工具返回的外部数据要么做内容过滤要么明确告诉模型“以下内容只是数据不是指令”。第四道审计边界。记录每次完整调用的输入、输出、工具参数、耗时、费用。审计不是保险柜而是事后定位问题和改进流程的唯一依据。没有日志的 Agent等于闭着眼睛开车。2.3 从常见的 Agent Harness 工程里可以学到什么社区里讨论较多的一些 harness 工程比如 codex harness、deepseek harness虽然具体形态和许可证各不相同但核心思路高度一致模型只负责在受限接口里产生决策真正的文件操作、命令执行、网络请求都经过 harness 的准入检查。这类工程给普通开发者的启发不是让你直接抄它们的代码而是让你建立一套属于自己的“准入清单”。我的建议是哪怕你的 Agent 只是一个内部工具也要先回答清楚这几个问题模型能不能访问网络如果能允许访问哪些域名模型能不能写文件如果能限定在哪个目录模型能不能执行命令如果能白名单命令有哪些单次任务允许跑多少步超了怎么处理每一步的日志落在哪里谁有权限查看提醒不要在第一步就追求“全自动”。先让 Agent 的每个危险动作都经过人工确认跑一段时间收集真实调用日志再决定哪些动作可以放权。3. LangGraph用状态图把 Agent 流程变成看得懂的工程3.1 为什么不是 LangChain 而是 LangGraphLangChain 是最早把“大模型应用开发”变成一套标准组件库的框架里面有 Chain、Prompt Template、Memory、Agent 等概念。但用久了你会发现一个问题Chain 是线性或简单的串联结构一旦你的流程是“有条件的、有循环的、有分支的、可能需要并行”的Chain 的抽象就有点不够用了。LangGraph 的出现本质上是把 Agent 流程从“链式调用”升级成“状态图”。它由 LangChain 团队维护核心思想很直接把你的 Agent 流程建模成一张图图里有节点节点之间是边边可以是普通边也可以是条件边。节点跑函数函数读写状态状态驱动路由。换句话说LangGraph 不是 LangChain 的替换品它是比 LangChain 更底层的编排基础设施。你依然可以使用 LangChain 的模型封装、prompt 模板、文档加载器只是把流程控制交还给图。3.2 核心概念和最小代码结构LangGraph 的关键概念可以压缩成四个State、Node、Edge、Conditional Edge。State贯穿整个图的共享状态通常是一个 TypedDict可以是消息列表、中间结果、计数器等。Node一个 Python 函数输入 state输出更新后的部分 state。Edge从一个节点到另一个节点的固定连接。Conditional Edge根据 state 内容动态决定下一个节点走哪里。一个最小 Agent 图结构通常是这样的from typing import Annotated, TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] def call_model(state: AgentState): # 这里调用模型拿到 response response llm_with_tools.invoke(state[messages]) return {messages: [response]} def call_tool(state: AgentState): # 解析工具调用执行工具把结果放回 messages return {messages: [tool_result]} def should_continue(state: AgentState): last state[messages][-1] if getattr(last, tool_calls, None): return tools return END graph StateGraph(AgentState) graph.add_node(model, call_model) graph.add_node(tools, call_tool) graph.add_edge(START, model) graph.add_conditional_edges(model, should_continue, {tools: tools, END: END}) graph.add_edge(tools, model) app graph.compile()这段代码是常见的“模型-工具循环”骨架不是某个版本的官方样例但整体结构在多数 LangGraph 版本里几乎不会有太大变化模型节点判断是否要调工具要调就进工具节点工具结果回到模型节点直到模型说“我完成了”。关键点在于add_messages这个 reducer。它告诉 LangGraph每次节点的返回值不要覆盖旧消息而是追加到消息列表里。这样状态天然记录了完整的对话历史循环才不会丢失上下文。3.3 分支、循环、子图从线性到复杂流程真实 Agent 不会永远只是“模型-工具-模型”的循环。拆成更有意思的场景分支控制模型判断任务类型走不同的处理管线。比如“查询类任务”走检索节点“生成类任务”走写作节点。用add_conditional_edges就能实现。循环检测有些任务模型会反复调同一个工具迟迟不收敛。可以在 state 里加一个计数器超过阈值就强制进入总结节点或者直接终止。并行分支多个独立子任务可以并行跑。LangGraph 的扇出fan-out结构允许一个节点分出多条路径最后汇总到一个节点。子图一个复杂流程可以拆成多个子图子图可以作为一个节点被父图调用。这个能力对团队协作非常重要——每个子图交给不同人维护父图只管串起来。从工程演进来看我的建议是先不用把图设计得很复杂。一个线性循环足够解决 80% 的初版需求。等到你真的需要“计划-执行-反思”这种多阶段结构时再引入分支和子图。过早抽象是另一种浪费。3.4 这里最容易误解的一个点很多人以为 LangGraph 里的“图”就是工作流引擎节点只能串行执行。其实 LangGraph 的节点本质是 Python 函数它不限制你在一个节点内部做什么。你可以在一个节点里做批处理、调外部 API、跑一段内部计算也可以让多个节点并行执行。图只负责状态流转真正的计算逻辑仍然在你的代码里。另一个容易误解的点是LangGraph 不等于 Agent。它只是一个编排库。你可以用 LangGraph 做一个完全没有模型参与的纯工作流也可以做非常复杂的多智能体协作系统。它是工具不是立场。4. MCP给 Agent 的工具接入定一套“标准插座”4.1 没有 MCP 之前工具接入长什么样在 MCP 成为话题之前让 Agent 调用工具标准做法是 function calling你在 API 请求里声明一个 tools 数组描述工具的 json schema模型决定调哪个工具返回结构化参数你的代码负责执行。这个流程本身没问题问题出在“每个工具都要单独实现一套接入逻辑”。你的 Agent 要接一个内部 API写一段 adapter要接数据库写一段查询封装要接设计稿平台再写一段。每个平台有自己的认证方式、数据格式和调用约定。团队里每多一个 Agent 项目这些 adapter 就得复制粘贴一遍。MCP 要解决的正是这个“重复开发”和“接口碎片化”的问题。4.2 MCP 的三件套协议、Server、ClientMCPModel Context Protocol是一个开放协议用来标准化“大模型应用如何连接外部工具和数据源”。它选用了 JSON-RPC 2.0 作为消息格式整个体系可以拆成三部分MCP Host运行 Agent 的应用比如你自己的 Agent 服务。MCP ClientHost 内部负责和 Server 通信的客户端组件。MCP Server暴露工具、资源、提示词的服务端可以是一个独立进程也可以是一个远程服务。Server 能暴露三类能力Tools可执行的函数、Resources可读取的数据文件/上下文、Prompts可复用的提示词模板。其中 Tools 是最核心的因为 Agent 的主要动作就是“基于决策调用工具”。一个用 FastMCP 写的最小 Server 长这样示例结构from mcp.server.fastmcp import FastMCP mcp FastMCP(demo-server) mcp.tool() def get_weather(city: str) - str: 查询城市天气 return f{city}晴25℃ if __name__ __main__: mcp.run()然后在 Agent 端用适配器加载这个 Server 的工具就能像普通 function calling 一样调用它。接入逻辑被压缩成“连上 Server、拿到工具列表、调用工具”三步而不是每个工具手写一套。4.3 Agent Skill 和 MCP 到底有什么区别这是最近很多人问的问题。Agent Skill“智能体技能”和 MCP 看起来很相似都是让 Agent 能做更多事情但它们的定位完全不一样。MCP 是“连接协议”解决的是 Agent 如何调用外部工具和数据资源。它规定的是通信格式、生命周期、工具定义的标准化。Agent Skill 是“能力包”通常包含一段精心设计的指令、使用步骤、示例、可能还有配套脚本或资源。它教的是 Agent “怎么做一件事”是一种可以复用的行为模板。打个比方MCP 是标准的电源插座定义好了接口规格Skill 是一本“操作手册工具包”告诉 Agent 做一顿饭有哪些步骤、用什么工具、注意什么。一个负责“接到电”一个负责“会做饭”。两者不冲突实际项目里常常配合使用MCP 提供工具接入Skill 提供使用这些工具的方法论。4.4 接入 MCP 时最容易踩的三个坑第一个坑工具描述写得过于模糊。MCP 暴露的工具会拼进模型上下文如果描述不清晰模型要么不会调用要么调用错参数。写工具描述时至少说明这个工具是干嘛的、什么场景用什么场景不用、参数格式是什么。第二个坑没有处理错误返回。模型调用工具工具返回的可能是一个 JSON 错误对象。如果你不把错误信息转换成模型能理解的文本模型下一轮就会基于错误的中间结果继续决策整个任务越跑越偏。正确的做法是工具节点捕获异常把“错误信息 建议重试方式”作为文本返回给模型。第三个坑把 MCP 当成万能胶。MCP 适合“外部工具/数据源接入”但如果你的 Agent 需要高频率、低延迟调用内部函数直接进程内调用往往比走 MCP 通信更划算。MCP 的价值在标准化和可复用代价是通信开销和复杂度。我的判断是跨团队、跨系统、需要复用的工具走 MCPAgent 内部的高频私有逻辑先留在代码里。提醒接入外部 MCP Server 时先审查它暴露了哪些工具、有没有文件写入或命令执行能力。外部工具进入你的 Agent等于进入你的信任域。5. 手把手搭一个带安全边界的最小 Agent5.1 环境准备先列一个最小环境清单不绑定具体版本落地前以你本机实际安装为准Python 3.10 或更高版本LangGraph 相关依赖一个模型 API优先选择 OpenAI 兼容接口方便调试MCP Python SDKmcp如果 LangGraph 配合 MCP通常还会用到langchain-mcp-adapters之类的适配层安装依赖用 pip 或 uv 都行这不是重点。重点是把环境拆成独立的虚拟环境避免和系统 Python 混在一起。5.2 先定义工具再定义流程顺序很重要。很多新手一上来就写图结果工具还没定义好图的节点逻辑就没法定。先写工具第一步确定 Agent 需要哪些工具。宁可少不要多。一个初版 Agent 有 2 到 3 个工具就很合适。第二步为每个工具写清楚描述、参数 schema、返回格式。第三步把工具包装成模型能调用的接口这一步可以直接用 function calling 声明也可以用 MCP。5.3 配置模型调用和工具节点这里不贴完整项目代码因为涉及模型 API key 和具体环境但结构可以讲清楚模型节点接收状态里的消息列表调用模型。如果用了bind_tools或 tool calling模型返回的响应里可能带tool_calls。工具节点遍历tool_calls逐个执行把结果转成 ToolMessage 追加到状态里。条件边判断最后一条消息是否还有tool_calls。有进工具节点没有进 END。5.4 单条样例验证不要一上来就接一堆工具、跑完整流程。先拿一条最简单的样例验证四个问题模型能不能正确识别“需要调用工具”工具调用参数是否被正确解析工具执行结果能否回到模型上下文循环结束时结果是否正确输出我的习惯是先只用一个工具且这个工具直接返回固定字符串。跑通之后再换真实工具再加 MCP。5.5 加安全边界安全边界不是最后才装的功能而是从第一次跑通后就要加上的结构。最小安全边界至少包括最大迭代次数比如 5 步超过直接终止并返回当前进度。工具白名单只允许模型调用已经注册的工具。超时控制每个工具节点执行设置超时超时返回错误消息。日志落盘记录每一步的模型输入输出、工具调用参数和结果。把这些边界做成一个SafetyLimits配置对象而不是散落在各个节点里。后面调优时只改配置不碰逻辑。5.6 看日志Agent 跑得对不对日志说了算这个阶段最重要的工作不是继续加功能而是站在日志前复盘一次任务的全过程。你要能看到模型在哪一步决定调工具为什么在那一句之后调。工具返回了什么模型如何消化这个结果。如果任务跑偏是模型误解了工具输出还是工具本身返回了模糊结果。没有日志你永远只能猜测 Agent 为什么表现不佳。“可观测性”听起来像是生产环境才需要的词但小项目更要在早期养成这个习惯——因为现在改成本低等项目复杂了再补日志改动面会大很多。6. 底层源码应该怎么看别被“源码”两个字吓住6.1 读源码的正确顺序很多人一听到“底层源码”就想着从头到尾读一遍仓库这个思路容易劝退自己。源码是给你查的不是给你背的。正确顺序是先读官方文档的架构说明知道这个项目有哪些核心模块。按“入口 → 核心对象 → 扩展点”的顺序切入。带着问题读而不是漫无目的地读。比如“State 是怎么合并的”“条件边是怎么路由的”。读的时候对照实际运行日志理解每段代码在真实调用里承担什么角色。6.2 LangGraph 源码里最值得看的模块如果你只想看几个关键点我建议优先看这些StateGraph 的构建和编译流程理解add_node、add_edge、compile到底做了什么。状态合并逻辑理解 reducer 的调用时机以及为什么add_messages能累积消息。Conditional Edge 的解析流程理解条件函数返回值如何转成路由。checkpoint / 持久化机制理解 Agent 如何保存和恢复状态。这些模块加起来没有多少代码但它们决定了 LangGraph 的行为边界。看懂之后你会明白为什么某些写法支持、某些写法不支持。6.3 MCP SDK 源码里最值得看的模块MCP SDK 源码的重点不太一样。我建议关注协议层JSON-RPC 消息如何封装、请求和响应的生命周期。会话管理Client 和 Server 如何握手、初始化、保持连接。工具注册与发现Server 端如何把mcp.tool()装饰的函数变成协议里的工具定义。传输层stdio、sse 等传输方式如何选择和切换。看这部分源码最有价值的收获是理解“一个外部工具从注册到被模型调用中间经历了哪些环节”。理解了环节遇到协议错误、超时、参数序列化问题时你就知道该去哪里排查。6.4 从会用源码到能改源码的分界线会读源码和会改源码是两回事。我的
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进