尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
LangGraph技能化改造:从提示词膨胀到结构化Skills的Agent架构实践
上个月我把一个几乎跑不动的 Agent 项目整个推倒重做了。起因很现实system prompt 里塞了十几个工具的调用说明模型经常在查订单和查物流之间犯迷糊A 需求改一个字段B 流程跟着出幺蛾子。重构的核心动作只有一个——在 LangGraph 里把所有能力收编成结构化的 Skills让模型通过技能注册表去选择能力而不是靠一段越来越长的提示词去猜。这篇文章把这段经历整理出来围绕三个问题展开Skill 到底解决了什么、LangGraph 里怎么给 Skill 安排位置、上线之后怎么排错和维护。从没接触过 LangGraph 的读者也能读我会把每个为什么这么做都讲清楚已经在折腾 Agent 的老手可以直接跳到第 5、6 节那里有我踩了几次才想明白的东西。1. 单块式 Agent 的失控现场Skills 到底在解决什么问题1.1 提示词膨胀不是数量问题而是耦合问题很多人把 Agent 能力不足归因于工具太少于是疯狂往提示词里加工具但问题恰恰相反工具越多、说明越杂模型越无从下手。一个三千多词的 system prompt里面混着工具定义、业务规则、输出格式、few-shot 示例它们彼此之间没有任何边界。比如你让模型先查天气再安排行程模型可能直接在第一步就把行程安排一起生成了因为安排行程的说明就在同一个上下文里触手可及。提示词膨胀背后的本质是耦合能力边界不清、触发条件互斥、更新互相干扰。加一个新工具所有既有描述都要跟着重写一遍回归测试等于全量回归。这种时候问题已经不是提示词工程能解决的了得靠架构层面的拆分。我见过太多团队在 5000 词的提示词上打补丁最后连自己都不敢改因为改一句就可能让另一个功能悄悄失效。1.2 技能Skill与普通工具的本质区别普通工具是最小原子动作输入输出都很薄比如执行搜索发送消息。而 Skill 是一整套可以独立交付的能力内部可能用了多个工具、多轮模型调用、甚至一段确定性代码但对外只暴露一个清晰的能力描述和输入输出契约。打个比方工具是单个零件Skill 是一台插上电就能工作的设备。在 Agent 架构里Skill 的价值体现在三个方面边界清晰模型只需要知道什么场景下用它不需要理解内部实现可测试每个 Skill 都能脱离整个 Agent 单独跑单测可组合路由层可以把多个 Skill 拼成更复杂的流程而不需要改 Skill 本身。对比维度普通工具Skill粒度原子操作完整流程外部可见内容函数签名技能说明 输入输出契约内部独立性通常依赖调用方上下文完全自包含适合场景简单查询、写操作多步骤、多工具组合、需要专门提示词的场景1.3 有些东西不值得做成 Skill要克制。一个只在单处出现、逻辑少于十行、输入输出不确定的 helper留在 node 里比做成 Skill 更划算。Skill 是有代价的要写模型能看懂的描述描述写得不好模型不会用要维护 schema字段一变全链路跟着变还要在图里多一跳节点增加调用链延迟。我给自己定了三条准入规则三个都是肯定答案才动手它会被两个以上场景复用吗它内部的逻辑足够独立、能离开整个图单独解释吗它的输入输出稳定吗过度抽象害死人把一个只调用一次的函数包装成 Skill只会让图和目录结构都变臃肿。2. 用 LangGraph 的 State 和 Edge 给 Skill 腾位置2.1 State 里专门开一条技能通道LangGraph 的所有节点共享一份 State数据怎么更新决定了你会不会踩坑。如果技能节点直接往全局字段里写业务数据多个技能在同一条链路上执行时就会出现上一手写的东西被下一手覆盖的模糊地带。我的做法是在 State 里固定两个字段skill_requests和skill_results。skill_requests是路由层给技能下发的指令列表skill_results是 dictkey 是技能名value 是该技能返回的结构化结果。所有技能只往自己名字对应的 key 里写读也只读自己的 key。这样整个图的数据流是单向的路由层决定谁被调用技能层只负责产出汇总层再统一消费结果。from typing import TypedDict, Any class AgentState(TypedDict): task: str # 用户原始任务 skill_requests: list[dict] # 待执行的技能请求 skill_results: dict[str, Any] # 技能名 - 结构化结果 messages: list[dict] # 对话记录用于回放和调试我把这称为技能通道。它的价值在于不管以后往图里加多少个技能数据流的骨架不需要改。新技能只是往skill_results里多占一个 key仅此而已。2.2 节点注册的三种层级在 LangGraph 里集成 Skills节点注册有三种做法我按项目演进顺序讲第一层直接在构建图时写graph.add_node(fetch_news, fetch_news_fn)。最直接但技能一多组装代码会膨胀成一大坨。第二层把所有技能放进一个注册表用循环统一注册。这一步让技能清单变成了数据而不是散落的代码SKILLS { fetch_news: { module: skills.fetch_news, enabled: True }, summarize: { module: skills.summarize, enabled: True }, # ... } for name, meta in SKILLS.items(): if meta[enabled]: node_fn load_skill_executor(meta[module]) graph.add_node(f{name}_node, make_adapter(node_fn, name))第三层把注册表结合配置文件做动态装配。测试环境加载全部技能生产环境只加载enabled: true的技能。关键点是把技能清单变成配置的一部分而不是埋在代码里后面做灰度、做开关才有的放矢。2.3 条件边才是真正做调度的人LangGraph 条件边的工作方式是执行完一个节点后调用一个函数根据返回值跳到对应的节点或 END。我习惯把这层叫调度器。它做的事情很纯粹查看当前 state 里的skill_requests取出第一个待执行技能的名字返回给条件边映射表。为什么不能让技能自己决定我做完去哪?因为那样技能就必须知道整个图的拓扑又耦合了。调度器统一收口之后技能节点完全不关心下一步是谁只管把手头的事做完。from langgraph.graph import StateGraph, START, END def route_after_skill(state: AgentState) - str: if state[skill_requests]: return state[skill_requests][0][name] return finish graph.add_conditional_edges( router, route_after_skill, { fetch_news: fetch_news_node, summarize: summarize_node, risk_alert: risk_alert_node, finish: END, }, )写完这段代码我才真正理解了 LangGraph 的设计哲学它不替你做调度决策它只给你一个干净的路由机制把什么时候跑什么能力完全交给你的逻辑。3. 技能工厂设计一份 manifest、一个执行器、一张注册表3.1 技能说明书manifest模型靠它理解世界在 LangGraph 里集成 Skills最不能省的是那份给模型看的说明书。我把它做成每个技能目录里的 manifest 文件JSON 格式包含 name、description、input_schema、output_schema。description 是给路由模型看的写作要遵守场景前置 行为描述 边界提示的格式{ name: fetch_news, version: 1.2.0, description: 当用户需要最新行业动态、资讯列表时使用。本技能只负责检索资讯标题和来源不做摘要、筛选和风险判断。如果用户需要对资讯进行总结请改用 summarize 技能。, input_schema: { topic: { type: string, required: true }, max_items: { type: integer, default: 5 } }, output_schema: { items: [ { title: string, source: string, published: string } ] } }千万不要只写抓取资讯四个字。我第一次就栽在这上面模型拿到这种描述完全不知道什么时候该用它。另外 input_schema 要严格一点模型填参数时很爱省略字段required 标不好后面 executor 就要到处处理 None。3.2 执行器executor纯函数化是底线每个技能目录里有个executor.py对外只暴露run(inputs: dict, ctx: dict) - dict。ctx 里放只读的环境信息比如 API key、base_url但绝不把整个 LangGraph state 传进去。为什么这么设计纯函数化之后技能单测可以在完全离线、不启动 LangGraph 的环境下跑直接在 pytest 里构造一个 inputs 就能验证。这是技能库能规模化维护的根本。换模型、换路由都不影响技能内部逻辑回归测试基本不用动。def run(inputs: dict, ctx: dict) - dict: topic inputs[topic] limit inputs.get(max_items, 5) items search_provider.fetch(topic, limitlimit) return { items: [ {title: item.title, source: item.source, published: item.published} for item in items ] }3.3 注册表与启动时的动态装配启动时扫描技能目录读 manifest验证 schema把 executor 包成符合 LangGraph node 签名的函数注册进图里。同时把所有 manifest 的 name description 汇总成一个工具列表绑定给路由模型。这里有个细节技能包成节点后函数签名需要接收 state 并返回 state而 executor 只处理自己的 inputs。中间必须加一层 adapter在 adapter 里从 state 取参数、调 executor、把结果写回skill_results[skill_name]:def make_adapter(executor_fn: callable, skill_name: str): def adapter(state: AgentState) - AgentState: # 取出当前技能请求执行完后移出队列 request state[skill_requests].pop(0) try: result executor_fn(request[arguments], {env: ...}) state[skill_results][skill_name] result except Exception as exc: state[skill_results][skill_name] { error: {code: type(exc).__name__, message: str(exc)} } return state return adapter这个 adapter 是通用模板代码所有技能共用同一套。反正我从这个函数开始写第一版技能体系后面的技能几乎都是复制目录改 manifest。4. 实战集成给行业动态速报Agent 挂上三个技能4.1 场景拆解任务的三个子问题我以模拟项目X为例它是一个行业动态速报Agent。用户给一个主题最终要输出一份简报这个任务可以拆成三个子问题抓取该主题的最新资讯、逐条生成摘要、识别其中的风险点并提示。这三个子问题我直接做成三个技能fetch_news、summarize、risk_alert。按 Skill 的标准它们都独立可复用fetch_news 可以被舆情监控复用summarize 能给邮件简报用risk_alert 也能单独接进别的流程。这个拆分是会被反复复用的典型不是拍脑袋抽象出来的。4.2 状态定义与三个技能节点状态沿用第 2 节的 AgentState。三个技能用同一个 adapter 注册成节点。summarize 内部是模型调用输入 items 列表输出每条摘要risk_alert 内部是规则 模型判断输入原文和摘要输出风险等级与理由。graph StateGraph(AgentState) graph.add_node(router, router_node) graph.add_node(fetch_news_node, make_adapter(fetch_news_executor.run, fetch_news)) graph.add_node(summarize_node, make_adapter(summarize_executor.run, summarize)) graph.add_node(risk_alert_node, make_adapter(risk_alert_executor.run, risk_alert)) graph.add_edge(START, router) graph.add_conditional_edges( router, route_after_skill, { fetch_news: fetch_news_node, summarize: summarize_node, risk_alert: risk_alert_node, finish: END, }, ) # 技能执行完回到路由继续排队 for node_name in [fetch_news_node, summarize_node, risk_alert_node]: graph.add_edge(node_name, router)注意这里我把技能执行完都拉回 router形成一个循环。调度的核心设计是路由节点一次性产出多个技能请求然后逐个执行。上一个技能结束不代表整个任务结束而是回到路由看是否还有待办。4.3 路由模型先选技能再填参数路由节点做的事情是把三个技能的 manifest 转成模型可识别的 tool schema绑定给模型调用一次要求返回 tool_calls再把这些调用写入skill_requests。def router_node(state: AgentState) - AgentState: tool_defs [manifest_to_tool_schema(m) for m in reg.all_manifests()] llm create_llm().bind_tools(tool_defs) response llm.invoke([ {role: user, content: state[task]} ]) state[skill_requests] [ {name: call.name, arguments: call.args} for call in response.tool_calls ] return state这样设计的好处是路由模型不需要知道每个技能内部怎么跑它只需要做选择题——根据用户任务挑出需要的技能并填好参数。选择题比开放式生成稳定得多这是整个架构最值钱的地方。4.4 跑通后的观察token 与稳定性几个真实体感供参考。system prompt 从原来的四五千字压到六百字左右路由模型只需吃一份技能描述列表上下文明显变轻代价是每次路由多一次模型调用但换来的是工具选择稳定很多。三个技能串行执行时比较头疼的是 summarize 阶段模型容易忘掉 fetch_news 的原文内容解决方式是把上一步结果合并进下一步的对话上下文而不是只靠 state 传结构化数据。诚实地说技能化不是银弹。它解决了耦合问题但没解决上下文管理问题。技能之间的数据怎么转换成模型可读的上下文仍然需要针对每一条链路单独调。这一块没有通用解法只能靠经验和观测数据。5. 排错实录技能不触发、状态串写、异常冒泡5.1 症状一技能被当成普通工具反复触发现象模型在一个任务里连续调用同一个技能两三次或者明明该走风险识别却反复去抓取资讯。我排查的第一件事是看路由模型的 tool_calls 日志结果发现模型选了技能之后根本不看 description纯粹按名字猜。根因是 manifest 的 description 写得太粗糙。我一开始写的是抓取资讯没有任何场景边界。改成带有前置条件的描述后误调用基本消失。后来我总结出一个通用模板当用户需要【场景】时使用本技能。本技能只负责【行为】不负责【边界外的事】。如果遇到【互斥场景】请改用其他技能。这段模板里的三个空必须填实否则 description 就是不合格的。模型不是看不懂是你没给它足够的判断依据。5.2 症状二上一步的结果被下一步悄悄覆盖现象risk_alert 跑完fetch_news 的结果变成空列表了。我一开始以为是被并发覆盖回放 state 才发现两个技能的 adapter 都往同一个 key 写数据——因为新技能在注册时名字写错了覆盖到了已有技能的结果 key。教训有三点技能注册 key 必须全局唯一adapter 只允许写自己注册时的 name谁都不能碰别人的 key汇总层永远先读请求列表再读结果列表按请求顺序去取结果而不是按固定 key 取。这个坑非常隐蔽因为单技能测试看不到只有多技能串行时才炸。# 汇总层安全写法按请求顺序取结果 def aggregate_results(state: AgentState) - str: chain [] for req in state[skill_requests_trace]: result state[skill_results].get(req[name]) if result and error not in result: chain.append((req[name], result)) return format_report(chain)注意我在 state 里加了一个skill_requests_trace它只记录原始请求、不弹队列专门用来给汇总层做顺序还原。弹队列的skill_requests是给调度用的两者不冲突。5.3 症状三技能内部异常把整个图拖垮现象fetch_news 请求超时抛异常整个图直接中断用户等了个寂寞。第一次我天真地以为在 executor 里 try/except 就完事后来发现异常被吞掉之后模型不知道发生了什么开始瞎编内容比报错还难查。最佳实践是第 3 节 adapter 里的那层统一防护异常转换成结构化 error 字段写回skill_results然后让路由模型在汇总时看到 error 字段由模型决定是重试还是换路径。注意不要无脑自动重试要带着错误信息给模型判断否则外部服务超时的时候重试会成倍放大压力。5.4 通用排查法回放状态而不是盯着日志猜LangGraph 支持 checkpointer把每一步的 state 存下来。我排错最常用的操作是加载同一个线程 ID按步回放看skill_requests是什么时候变空的、skill_results是哪一步被覆盖的。这比翻一百行日志高效得多。我的固定排查流程是日志定位怀疑节点 - 回放 state 确认数据 - 用出问题的输入构造最小复现 - 修 manifest 或 adapter - 跑回归。这个流程走下来平均一个技能问题半小时内能定位比早期靠猜快了一个量级。6. 生产化阶段的血泪建议版本、成本和协作6.1 Skill 目录也要做版本管理与灰度技能是运行时代码会持续更新。我一开始把 manifest 和代码混在一个仓里改了个字段就全量上线路由模型的表现波动很大。后来在 manifest 里加了 version 字段并支持按环境加载技能清单测试环境加载全部技能生产环境只加载 enabled 的技能。这里有一个真实教训新版本尽量先加后删。如果新版本想改 description直接替换会有过渡期模型两边都不讨好。正确做法是先加一个 v2 技能旧技能保留观察一周调用成功率再下线 v1。技能下线前还要检查一下有没有其他流程引用了它的 schema我吃过一次亏删了技能之后汇总层直接报 KeyError。6.2 给每个技能记账token 成本的可观测化技能化之后每个技能的调用都是独立模型调用成本散落在图里不打点的话你对钱毫无概念。我在 state 里增加了 usage 聚合字段每次模型调用后把 prompt_tokens 和 completion_tokens 累加到当前技能名下。def record_usage(state: AgentState, skill_name: str, usage) - None: if usage not in state: state[usage] {} entry state[usage].setdefault(skill_name, {prompt_tokens: 0, completion_tokens: 0}) entry[prompt_tokens] usage.prompt_tokens entry[completion_tokens] usage.completion_tokens汇总层打印一份费用明细表之后我很快就发现 summarize 技能占了六成成本。有了数据才能谈优化不然只能拍脑袋换小模型。我建议每个技能上线时就带上这段打点逻辑不然后面补会非常痛苦。6.3 团队协作让描述能力差的人写描述是灾难技能化的一个红利是后端同事不用懂 LangGraph 也能提交技能但前提是 description 必须由最理解业务场景的人来写。我踩过的坑让后端同事按函数注释的习惯写技能描述结果模型把技能当摆设因为描述全是技术词汇完全没有什么时候该用我。后来我定了一个硬性模板描述里必须出现当用户需要…时和本技能不负责…这两类句式缺一个都不允许合入。这个模板没几行字但提交质量提升立竿见影。测试方面每个新技能必须附带一个最小用例和一组期望结果跑通才允许注册。最后分享一个个人小习惯每上线一个技能我会在它的目录下放一个 examples/ 文件夹把路由模型成功调用和失败误调用的完整日志都贴进去。过两周做回归测试时这些例子就是现成的测试集。技能化这条路难的不是写代码和搭图而是学会用边界思维去切分能力——边界切得越干净后面的路由、测试、协作就越省心。希望这篇把坑标出来的短文能让你少走我走过的弯路。
RELATED

相关推荐

Go语言实现微信公众号爬虫:批量采集方案与避坑指南

Go语言实现微信公众号爬虫:批量采集方案与避坑指南

简介:这是一个基于Go语言实现的微信公众号批量自动化爬虫核心项目,面向有爬虫开发经验的工程师或数据采集团队,用于高效抓取公众号全部文章,同时强调尊重原创与规避账号风险。项目采用模块化设计,可作为Go语言包直接引…

📅 2026/10/21 18:52:36
artcraft实战指南:从创意到成品的完整链路

artcraft实战指南:从创意到成品的完整链路

“artcraft”这个词,圈内人一看就懂——它把“Art”和“Craft”焊在一起,代表的不是单纯画画,也不是纯粹做手工,而是两者交叉地带的那套玩法。我做了这么多年创意类项目,最大的体会是:只懂艺术不懂工艺&…

📅 2026/10/21 18:52:36
从色彩管理到材质生成:构建数字艺术生产管线

从色彩管理到材质生成:构建数字艺术生产管线

1. 项目全貌:当“艺术感性”遇上“工艺理性” “artcraft”这个名字,拆开看就是 art 与 craft 的组合。我在搭建这个项目的时候,想得很直白:艺术创作不能只靠灵感,手工艺也不能只靠蛮力,两者得有一个桥梁&a…

📅 2026/10/21 18:52:36
MORE NEWS

更多资讯

📰

Java团队用LangChain4j打造医疗智能客服:多轮对话与RAG实战

做医疗智能客服这个项目的时候,我最大的感受是:AI框架的选型真的可以决定一个Java团队的开发节奏。我们内部代号叫"硅谷小智",说白了就是一个面向患者问诊前、问诊后的智能接待机器人,要能回答科室导诊、就诊流程、检查…

📰

多模态大模型训练避坑指南:归一化、激活函数与架构一致性

多模态大模型这几年从论文火到落地,但真正动手做训练和微调的人都知道,它和单模态模型完全是两码事。单模态下好用的很多套路,搬到多模态场景就会出各种奇奇怪怪的问题——loss震荡、收敛慢、某一个模态直接学废。我自己调试了几个开源多模态…

📰

diagram-design:让架构图成为可执行的技术契约

1. “diagram-design”不是画图软件的代名词,而是一套被严重低估的系统化表达方法论 “diagram-design”这个词组在搜索引擎里几乎不返回任何权威定义——它既不是某个知名工具的品牌名,也不属于ISO或IEEE标准术语库。但过去三年里,我在某高校…

📰

远程控制软件怎么选?五款主流远控工具实测对比

作为一个经常要在电脑、手机、平板之间来回切换的人,远程控制软件可以说是我日常工作里的刚需工具。前前后后我把网易UU远程控制、ToDesk、向日葵、TeamViewer、RustDesk这五款主流的专业远控软件都用了个遍,从游戏串流、远程办公到帮家里长辈修电脑&…

📰

系统架构师能力解剖:从截图、做题到目录的实战思维训练

1. 这不是考试指南,而是一份系统架构师实战能力的“解剖图谱” “系统架构设计师”这六个字,在很多人的认知里,还停留在“高级职称考试”“软考高项”“背书刷题拿证”的层面。但我在过去十年带过的几十个模拟项目X中反复验证过一个事实&…

📰

串口不够用?STM32L151+WK2124 SPI转4路串口驱动全记录

串口不够用这个事,做嵌入式的人迟早会撞上。最近我在维护一台低功耗仪表,主控就是标题里的STM32L151,板子上要接的设备却不少:两个传感器、一个显示屏、一个调试口,后端还想留一路备用。这颗芯片原生只带两个UART&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬