【LLM】用 LangGraph 构建 AI 网络调试助手
1. 前言顾得助手 是我个人开发的一款 Chrome 插件这篇文章将分享其中‘网络智能调试助手’功能的实现思路。这个功能主要用于拦截和Mock页面上的网络请求。1.1 为什么会开发这个功能在前端开发中我们经常会遇到这样的问题后端接口尚未完成前端无法联调某些异常场景500、延迟、空数据难以模拟线上问题难以在测试环境复现造数据浪费大量时间除了解决以上痛点功能还具备以下特点Mock 配置支持界面手动操作或自然语言指令生成支持 Mock 请求头、请求体、响应体、超时时间等Mock 配置规则持久化跟随会话不会自动删除支持多轮对话兼容fetch和XMLHttpRequest请求2. 界面展示顾得助手 对话界面双击输入框然后选择‘网络智能调试助手’进入。3. 整体架构3.1 架构分层在整个功能中我把 AI 的能力限制在了分析用户自然语言并生成 Mock 规则配置网络请求的拦截还是由确定性代码来实现。功能的整体架构分层如下本篇博客只介绍工作流编排层的实现。┌──────────────────────────────────────────┐ │UI交互层(Vue 组件)│ │ 接收用户输入展示AI响应 │ └──────────────────────────────────────────┘ ⬇️ ⬆️ ┌───────────────────────────────────────────┐ │工作流编排层(LangGraph)│ │ router → checker → switcher/consultant │ │ ↓ ↓ ↓ ↓ │ │ refiner presenter new_init...│ └───────────────────────────────────────────┘ ⬇️ ┌──────────────────────────────────────────┐ │AI模型层(LLM)│ │ 意图识别、内容生成、结果呈现 │ └──────────────────────────────────────────┘ ⬇️ ┌──────────────────────────────────────────┐ │ 数据持久化层 │ │ 规则存储、会话管理、拦截器步 │ └──────────────────────────────────────────┘3.2 为什么选择 LangGraph我们先看一个典型的用户交互场景。例如用户可能这样输入帮我拦截 /api/user 接口接口响应数据设置为 {code:200,msg:成功}系统需要解析用户意图并生成一条新的 API 规则。接着用户可能继续修改刚刚的配置不对将响应数据改成 {code:500,msg:失败}并设置超时时间为800ms此时系统不仅需要理解新的修改指令还需要找到当前正在编辑的 API 规则更新对应的响应数据增加新的配置项timeout而在真实使用场景中用户输入往往更加复杂。例如与 API 规则相关的配置操作对已有规则的修改或切换新规则的创建与系统的普通闲聊这意味着系统必须具备两种能力理解多种用户意图在多轮对话中保持稳定的状态如果仅使用单一 Prompt LLM 推理的方式很难稳定处理这类问题。因为Prompt 会随着场景增多而迅速膨胀逻辑控制完全依赖 LLM缺乏确定性多轮对话中的状态管理变得困难而LangGraph正好提供了一种更适合这类场景的解决方案。LangGraph 的核心特点包括显式的状态管理State可以在多轮对话中持续维护规则、当前编辑对象等上下文信息可控的执行流程Workflow通过 Graph 的节点和边来明确控制系统行为确定性的执行路径不同用户意图可以被路由到不同的处理节点因此对于这种场景复杂但结果相对确定的 AI 功能使用 LangGraph 可以让系统结构更加清晰也更容易维护和扩展。3.3 LangGraph 工作流整体架构基于上述需求我们使用LangGraph构建了一个可控的 AI 工作流。整个系统被拆分为多个职责清晰的节点通过 Graph 的方式组织执行流程。整体架构如下图所示这个架构的核心思想是让 AI 只负责“理解和生成”而所有“流程控制”由代码完成。3.4 GraphState 设计我们通过Annotation.Root定义全局状态结构 。constGraphStateAnnotation.Root({// 会话IDthreadId:Annotationstring(),// 用户输入信息userInput:Annotationstring(),// 意图类型: refine | switch | new_api | unknownintent:Annotationstring(),// 已配规则列表rules:AnnotationApiRule[](),// 当前操作的规则IDactiveId:Annotationstring(),// 正在生成的规则片段pendingRule:Annotationany(),// 历史对话信息方便判断用户真实意图history:Annotationany[]()});type GraphStatetypeofGraphChannels.State;拓展LangGraph 中Annotation是定义状态图State Graph数据结构的核心工具。用于指定每个状态字段的更新策略Reducer和默认值。Reducer 常见写法示例// 完全替换 (Replace)reducer:(_x,y)y// 追加合并 (Concat)reducer:(x,y)x.concat(y)// 对象合并 (Merge)reducer:(x,y)({...x,...y})3.5 节点解析下面我们逐个介绍每个节点的设计。3.5.1 Router 节点意图识别Router 是整个系统的入口节点负责判断用户当前想做什么。例如用户输入帮我拦截 /api/user 接口或者不对把返回值改成 {code:500}Router 需要识别出用户意图例如refine 修改/微调规则 switch 切换规则 new_api 新增规则 unknown 未知意图真实代码如下privateasyncrouterNode(state:GraphState):PromisePartialGraphState{constactiveRulestate.rules.find(rr.idstate.activeId);constactivePathactiveRule?activeRule.path:无;// 组装提示词提示词中强制 AI 输出结构化 JSONconstpromptdevNetSmartAssistantPrompts.router.replaceAll({{rulesJson}},JSON.stringify(state.rules,null,2)).replaceAll({{currentActiveId}},state.activeId||无).replaceAll({{currentActivePath}},activePath).replaceAll({{history}},this.formatHistory(state.history)).replaceAll({{userInput}},state.userInput);// 大模型调用constresawaitthis.callModel([{role:user,content:prompt}]);// 解析大模型返回的JSONconstresultthis.extractJson(res);return{intent:result.intent,activeId:result.targetId||state.activeId};}可以看到这里并没有直接让 AI 输出自然语言而是强制 AI 输出结构化 JSON。Prompt 示例你是一个网络调试助手需要识别用户意图。 当前已有规则 {{rulesJson}} 当前激活规则 id{{currentActiveId}} path{{currentActivePath}} 历史对话 {{history}} 用户输入 {{userInput}} 意图判定规则 1. new_api新增规则 - 触发场景用户表达“监听/增加/拦截/模拟”等动作且未明确指向列表中某个已存在的规则 - 补充场景用户正在完善未创建成功的新规则信息无论是否修改路径 - 返回要求path提取自用户输入无则nulltargetIdnull。 ...... 请分析用户意图并返回 JSON { intent: refine | switch | new_api | unknown, targetId: 规则ID可选, path: 接口路径可选, reason: 无法识别时的原因 } 不要输出任何解释只输出 JSON。3.5.2 Refiner 节点修改已有规则如果 Router 判断用户是refine意图则进入 Refiner 节点。典型输入把返回值改成 {code:500}Refiner 的任务是在现有规则基础上生成更新字段。代码实现privateasyncrefinerNode(state:GraphState):PromisePartialGraphState{consttargetRulestate.rules.find(rr.idstate.activeId);// 组装提示词constpromptdevNetSmartAssistantPrompts.refiner.replaceAll({{targetRuleJson}},JSON.stringify(targetRule,null,2)).replaceAll({{userInput}},state.userInput);// 模型调用constresawaitthis.callModel([{role:user,content:prompt}]);// 模型返回结果解析constupdateFieldsthis.extractJson(res);constnewRulesstate.rules.map(r// 替换模型输出的更新字段r.idstate.activeId?{...r,...updateFields}:r);return{rules:newRules};}这里 AI 的角色是限制只生成更新的字段而不是生成完整对象。Prompt 示例你是一个 API 规则微调专家。用户想要修改现有的 API 拦截规则。 当前规则 {{targetRuleJson}} 用户输入 {{userInput}} 请根据用户要求修改规则并返回需要更新的字段 字段说明 - responseBody: 响应体内容拦截后返回给前端的数据。 - delay: 模拟延迟时间数字单位 ms拦截后延迟多久返回响应 ...... 示例 { responseBody: {\code\:500,\msg\:\失败\}, delay: 800, ...... } 只返回 JSON不要解释。3.5.3 Checker 节点规则匹配Checker 节点负责解决一个现实问题用户说的 API 是否已经存在例如拦截 /api/user系统需要判断是已有规则还是新规则代码节选privateasynccheckerNode(state:GraphState):PromisePartialGraphState{letactiveIdstate.activeId;// 校验 activeId 真实性防止过时的ID或AI幻觉if(activeId!state.rules.some(rr.idactiveId)){activeId;}constpathstate.pendingRule?.path||;if(path){constexactMatchstate.rules.find(rr.pathpath);if(exactMatch){return{activeId:exactMatch.id};}// 如果意图是切换或查看允许模糊匹配if(state.intentswitch||state.intentrefine){constfuzzyMatchstate.rules.find(rr.path.includes(path)||path.includes(r.path));if(fuzzyMatch){return{activeId:fuzzyMatch.id};}}}return{activeId:};}注意这里使用纯代码路径匹配属于确定性逻辑不用交给 LLM。3.5.4 Consultant 节点补全规则如果用户信息不完整例如拦截 /api/user但没有说明返回内容。这时 Consultant 节点会解析已有信息判断是否完整引导用户补充代码节选privateasyncconsultantNode(state:GraphState):PromisePartialGraphState{constpromptdevNetSmartAssistantPrompts.consultant.replaceAll({{path}},state.pendingRule?.path||).replaceAll({{userInput}},state.userInput);constresawaitthis.callModel([{role:user,content:prompt}]);constresultthis.extractJson(res);return{pendingRule:result.pendingRule,isComplete:result.isComplete,markdownOutput:result.reply};}Prompt 示例用户希望创建一个新的 API Mock 规则。 当前信息 path: {{path}} 用户输入 {{userInput}} 请提取规则信息并判断是否完整。 返回 JSON { pendingRule: { path: , responseBody: , delay: 0 }, isComplete: true, reply: 如果信息不完整需要向用户提问 }3.5.5 NewInit 节点创建规则当 Consultant 判断信息完整后就进入NewInit节点。这里不再使用 AI而是由程序创建规则对象privateasyncnewInitNode(state:GraphState):PromisePartialGraphState{// 组装最终生成的规则对象constnewRule:ApiRule{id:uuidv4(),sessionId:state.threadId,path:state.pendingRule.path||,active:true,mode:mock,responseBody:state.pendingRule.responseBody||,delay:state.pendingRule.delay||0};return{rules:[...state.rules,newRule],activeId:newRule.id};}核心原则AI 只负责理解需求最终数据结构由代码生成。3.5.6 Presenter 节点结果生成最后由 Presenter 统一输出结果。privateasyncpresenterNode(state:GraphState):PromisePartialGraphState{constpromptdevNetSmartAssistantPrompts.presenter.replaceAll({{intent}},state.intent).replaceAll({{resultInfo}},state.status).replaceAll({{userInput}},state.userInput);constresawaitthis.callModel([{role:user,content:prompt}]);return{markdownOutput:res};}Prompt 示例你是一个友好的 AI 助手。请根据当前的处理结果为用户生成一段总结性的回复。 用户意图: {{intent}} 用户输入 {{userInput}} 当前操作结果 {{resultInfo}} 回复要求 1. 回复整体使用 Markdown 格式呈现。 2. 内容简洁明了禁止直接返回 JSON 格式的代码块。 3. 规则优先级按顺序匹配符合一条后停止 - 优先级1xxx ...... 请直接生成符合上述要求的回复内容3.6 构建 Graph完整代码privatebuildGraph(){constgnewStateGraph(GraphChannels).addNode(router,(s:GraphState)this.routerNode(s)).addNode(refiner,(s:GraphState)this.refinerNode(s)).addNode(checker,(s:GraphState)this.checkerNode(s)).addNode(switcher,(s:GraphState)this.switcherNode(s)).addNode(consultant,(s:GraphState)this.consultantNode(s)).addNode(new_init,(s:GraphState)this.newInitNode(s)).addNode(presenter,(s:GraphState)this.presenterNode(s)).addEdge(START,router)// 意图跳转逻辑.addConditionalEdges(router,(s:GraphState){if(s.intentrefine){// 微调已有规则returnrefiner;}if(s.intentswitch||s.intentnew_api){// 新增规则或切换规则returnchecker;}returnpresenter;})// Checker 跳转逻辑.addConditionalEdges(checker,(s:GraphState){if(s.activeId){// 规则存在切换规则returnswitcher;}// 规则不存在引导用户确认规则returnconsultant;}).addEdge(refiner,presenter).addEdge(switcher,presenter)// Consultant 跳转逻辑只有信息完整才初始化.addConditionalEdges(consultant,(s:GraphState){if(s.isComplete){returnnew_init;}returnpresenter;}).addEdge(new_init,presenter).addEdge(presenter,END);returng.compile();}4. 关键设计总结这个 LangGraph 工作流有三个非常重要的工程原则4.1 AI 只做理解AI 只负责意图识别规则解析信息补全不负责状态管理数据结构流程控制4.2 流程控制由代码完成所有逻辑判断都在 Graph 中router - checker - consultant - new_init而不是 Prompt 中。4.3 强制结构化输出流程程中 AI 输出必须是 JSON否则直接丢弃。这样可以大幅降低幻觉格式错误Prompt 不稳定5.遇到的问题5.1 虽然提示词中限制输出JSON但LLM有时不按约定返回虽然在提示词中强制了大模型必须输出 JSON 格式内容但总有特例LLM不按照规定格式输出比如返回好的{JSON内容} 或者 输出的内容使用 json 包裹下面是几种兜底方式调用模型时传入 response_format 参数需API支持强制模型返回合法的JSON格式字符串。{model:gpt-4o,messages:[{role:system,content:你是一个助手请输出用户信息的JSON。},{role:user,content:用户叫张三25岁。}],response_format:{type:json_object}}从回答中提取出JSON格式字符串。如果以上两步获取后的JSON还是解析失败增加重试机制让LLM自动纠错并输出正确内容Prompt示例下面这段文本本应是一个完整、可 JSON.parse 的对象但可能夹杂了说明文字、markdown 或语法错误。请只输出修复后的那一个 JSON 对象本身不要代码块、不要任何解释。\n\n {{模型输出的内容}}6. 参考文章● LangGraph 官方文档● ‘顾得助手’插件功能更新介绍