核心内容摘要
探索青春的奥秘:男生女生的那些“差差差”
本文详细介绍了langGraph框架的Nodes和Edges两大核心要素。
Nodes作为处理State的加工站点应遵循单一职责原则Edges作为连接节点的传送带分为普通边和条件边两种类型。
通过邮件起草Bot实例展示了如何使用Pydantic结构化LLM输出并将人类反馈融入Agent系统帮助读者构建具有交互能力的AI应用值得收藏学习。
FOMO时代应该学习什么当我想要手搓自己的Agent我开始研究最受欢迎的Agent开发框架之一langGraph。
虽然说对于很多生活中的小项目使用langGraph框架属于是**“杀鸡用牛刀”了但是我发现了解一些这个知名开源框架的设计思路**还是很有启发性——了解它们的过程就是向“最聪明的一批人”借鉴和学习的过程。
在AI能力如此强大的今天**“语法”真的不重要。
**因为不管你怎么用力的学习在几个月之后你学会的知识都会“失效”。
新的框架、新的语言、新的能力层出不穷他们在“社区”这个群体层面永无止境的进化着在我眼里早就已经超过了“个体”可以追赶的体量。
那么为什么我还会在这里写一下我关于langGraph的学习经历呢也许是我还觉得在这种洪流之下依然存在着一些“不变”吧。
技术的进化就跟生物的进化很像生态快速暴力穷举了很多个“变异形态”但是再复杂的变异都是以最初的几种基础模块作为起点的。
我想要了解的就是那些基础的模块。
这些基本的了解能够带来向上探索复杂形态的视野。
上一篇文章写了怎么理解langGraph里面的State这篇文章继续讲解Graph剩下的两大要素Nodes和Edges。
还会举一个“起草邮件Bot”的例子来讲述怎么把人类的反馈也加入。
这篇文章会包含NodesNormal Edges Conditional Edges利用Pydantic结构化LLM的输出怎么做一个有人类反馈的“邮件起草Bot”本篇文章包含一些Python基础知识需要理解类、实例、函数、列表、字典等概念字数6k阅读时间比较长感兴趣的朋友可以收藏或者转发~NodesGraph有3大基本要素State、Nodes和Edges。
其中State我在上一篇文章讲过它是一个贯穿全场的信息传递的媒介如果把Graph看成一个大公司那么State就是这个公司所有部门都可以读也可以写的**“共享记事本”**。
因为有这些共享信息公司所有的员工才能够有条不紊的互相配合。
也可以理解为整个Graph这个处理工厂处理的产品就是这个State。
详情请见手搓 Agent 必备langGraph概念入门一层一层剥开State是什么那么Node是什么呢用比喻来说可以把Node想象成一个**“机械臂/加工站点”**它接收State处理State把处理结果更新到State里面去。
我有的时候会纠结究竟什么时候应该设定一个Node。
请教了Gemini老师它是这样说的当你要更新State的时候你需要设定一个Node。
Node应该尽量去承担单一的职责这就跟我们写代码的时候要去考虑可复用性和可维护性一样只有职责拆分比较明确才能够不断用“基本模块”去构造“复杂模块”。
Node可以调用LLM也可以不调用Node本质是function嘛千变万化不受限制。
如果要调用LLM可以用**“单一人设”这样的原则来设置Node。
也就是说如果你在写提示词的时候发现这个Node很精分一会儿关注重点是A一会儿关注重点是B你就应该拆分Node了**。
在代码实现上Node可以理解为是一个Function实际上是Runnable。
既然是Function就会有输入和输出。
Node输入一般包括但是不限于State。
那么输出是什么呢这里归功于langGraph这个底层框架的努力让我们在写Node的时候可以只输出需要更新的**“增量部分”**而不必把整个State重新再抄写一遍。
举一个例子假如我的State是这样的在上篇文章里面解释过Annotated里面的第二个参数会被解析出来作为归并函数reducer functionState字段的更新方式由这个归并函数决定。
这里字段“message”和“count”都会被**“累加add”**。
而“memberTier”则会被不断“覆盖”。
那么我的Node可以写成这样sayHello函数只输出了“message”和“count”两个key——langGraph底层就会找到State里面这两个“key”把新的信息给**“累加上去”**。
假如我们的初始化State是下面这样在经过sayHello这个Node之后“message”会增加一条“count”也会1。
**如果这个Node没有输出跟State重叠的key那么它做了什么都不会被更新到State上面去。
**比如有的时候我就是用一个Node占位一下print一个句子State不会被改变。
在Graph中还有两个特殊的Node就是标志着“开始”的START和标志着“结束”的END。
这里用大写的英文字母表示因为这两个Node就是**“常量”**不会有改变也不用你自己去定义你只需要import langgraph里面已经定义好的这两个“常量”就可以。
无论你画的是多么简单的图你都需要“规定”这两个Node。
在实际上这两个Node什么其他影响也没有就只是标记着“开始”和“结束”。
为什么会需要这两个Node因为我们在运行一张图的时候也就是调用了invoke函数数据是有方向的、有顺序的。
不定义“开始”我们无法定义“顺序”。
在执行过程中langGraph底部一直在用**“循环”**搜索下一个Node是什么不定义“结束”这个“循环”就没有结束的条件。
Edges那么什么又是Edges呢它实际上就是一个**“调度字典”**。
他就是把各个不同的“机械臂”链接起来的“传送带”。
在langGraph中有两种基础Edge一种是Normal Edge一种是Conditional Edge。
它们在本质上都是一个**“字典”。
**模拟一下大概是这样的我们都知道“字典”无非储存的是一组Key: Value对应关系。
**普通边Normal Edges**储存的是固定的映射。
逻辑“如果A结束永远去B。
”这个字典的Key是上一个Node的“名字”Value是下一个Node的“名字”。
这里会使用**“名字”是因为它存的是一个“字符串”这个字符串是你显式的通过add_node函数**由langGraph提供的去定义的而不是直接的function name。
为什么要这样干呢还是为了解耦假如你突然想改这个Node了只要改一个名字的映射就可以了不用到处去改代码引用。
Conditional Edges**条件边Conditional Edges**依据不同的条件有不同的映射。
逻辑它接收一个函数Router的输出并根据这个**“函数的输出”**在字典中查找下一站。
当你要在一个Graph里面添加条件边它的语法是你需要传递上一个Node的“名字”一个用来判断条件的函数和一个映射地图。
上一个Node的名字是为了指出从哪里出发遵循关注点分离的原则这里用的是你显式定义的**“名字字符串”而不是function name**。
Router函数路由函数就是用来做判断的函数它的作用是依据XXX条件决定下一个Node是什么。
我们用是否获得VIP服务来举个例子需要判断“用户的身份”如果用户是VIP则走到VIP服务的Node否则就走到普通服务的Node。
我们写一个“判断用户身份”的函数checkVIP注意这个Router函数返回的是一个**“字符串”这个“字符串”往往代表着“意图”**不是直接返回一个Node的function name。
这也是我们需要在add_conditional_edges函数里面去传递path Map的原因。
path Map按照标准的写法这也是一个“字典”这个“字典”是把Router函数返回的**“意图字符串”给映射到对应的“Node名字”**上面这样就由“Router函数”决定了“下一个Node”是什么。
不过在langGraph的官网上举的例子这里传递的不是一个Dict而是一个List。
这是一种简写——把“意图字符串-Node名字”这一层映射给省略了。
在langGraph框架里会默默给你做成一个**“恒等映射Key和Value相等”**然后路由到对应的节点里面去。
Graph在了解langGraph的三大基本元素State、Nodes和Edges之后把这几个组合在一起就能够定义图Graph了。
定义图会分为以下几个步骤把State Class传递进去相当于告诉Graph这个大型工厂你可以加工的产品长得什么样子你可以读写的“共享记事本”长得什么样子添加Node相当于在Graph工厂把各种不同的“机械臂”给安装上去。
在底层其实就是Graph默默把Node的“名字”和“函数”对应关系记录好存起来。
添加Edges相当于把不同“机械臂”之间“传送带”给连接起来这样才知道“下一步应该往哪里去”。
知道哪里是开始哪里是结束。
定义完图之后需要把这个图compile一遍这样做的原因之一是在解析“字符串”把他们还原为“真实的函数对象”比如解析在上一篇文章里面讲过的“Annotated的第二个参数”。
接下来你就可以走入主程序逻辑了也就是“调用你编译好的图”。
这句的语法是注意要区分什么逻辑应该放在“主程序里面”什么逻辑应该放在“Graph里面”。
最为简单的
使用方法定义一个初始的State然后传递给你定义好的Graph再打印出处理后的结果做一个帮忙起草邮件的Bot在了解Graph相关的基本概念之后我们可以看一个简单的例子怎么创造一个帮我们“起草邮件的Bot”这里就涉及定义跟LLM交互的Node把人类的反馈融入进去这个Graph非常简单就只有1个自定义的Node这个Node是用来跟LLM对话的。
第一步我们来定义State思考一下如果要让LLM帮我们起草邮件那么一定要有的字段是什么messages用来传递消息无论是来自人的消息还是来自AI的回答。
category用来区分邮件的类别这里就定义只能够从[Business,Notification,Casual]中间列举出一个。
draft起草的邮件内容。
isConfirmed表明用户的意图如果用户确认这个邮件不需要修改了这里就写True否则就写False。
默认值为False。
这样可以形成一个不断依据用户建议修改的循环。
第二步我们来定义LLM的输出结构定义了State之后我们一定要LLM返回的内容跟我们需要的字段是相符合的不然的话没有办法顺滑的更新State。
这里就需要引入一个新的工具——Pydantic。
Pydantic是一个基于Python类型提示Type Hints的数据验证和设置管理工具。
在上篇文章中我们强调过它跟TypedDict非常相似但是它比TypedDict对于数据的约束力要更强。
它定义的是一个Python类(Class)但它具备特殊的“容器”属性。
普通字典(Dict)就像一个大塑料袋。
你往里装苹果、梨、甚至鞋子它都不会管。
取用时容易出错比如想拿苹果却摸到了鞋子。
Pydantic类(BaseModel)就像一个带格子的精密收纳盒。
圆形的格子只能放整数(int)方形的格子只能放字符串(str)。
如果你试图强行把方形物体塞进圆形格子盒子会直接把你的手弹开并报警抛出ValidationError。
Pydantic通过定义模型Models来实现具有下面的优势**运行时验证**当数据进入模型时Pydantic会自动检查类型。
如果数据不合法它会抛出清晰的错误。
**数据解析Parsing**它不只是验证还会尝试强制转换类型Coercion。
比如你要求int但传入了“123”它会自动帮你转成数字。
与IDE深度集成模型是标准的Python类因此在开发时可以享受完美的自动补全和类型检查。
**JSON序列化**可以轻松地将复杂的对象转换为字典或JSON字符串。
在AI工程中Pydantic是连接“不可控的LLM对话”与“严谨的代码逻辑”之间的护城河。
总而言之我们可以这样记忆Pydantic用来定义“数据结构”能够尽量去“强迫LLM”返回的内容符合要求。
我们需要LLM返回的数据结构是跟State对应的这里我们定义的数据结构叫做ProposerOutput这里注意最好要写description这个description不仅是给人看的还是给LLM看的写明确的description能够让LLM更加明确的理解需要生成的内容。
第三步定义系统提示词我们需要一个系统提示词来设定这个LLM的人设由于系统提示词一般都会不断调整我会把它定义在Node外面方便集中微调第四步定义Node人类的反馈首先我要做一个大逆不道的操作——那就是把人类的反馈放在这个Node节点里面使用input函数就可以形成一个“可交互”的节点。
这是为了简化逻辑实际生产中不会这么用用input函数承接“人类的输入”然后使用langGraph的Message组件**“HumanMessage”**形成“标准化的消息”加入到State的messages字段。
后面还会用到SystemMessage和AIMessage。
这些Message类主要的作用还是**“包装”——就是把真实的内容包装成AI API需要的格式。
让用户可以不用关注解析格式问题**只用把内容str写进去就好。
之所以生产不这么用是因为一旦把input写在Node里面只要人类不回应这个线程就会被卡住了后面的一切都无法运行还不会释放内存**这样非常不妙**下篇文章会讲解一下到底应该怎么避免这种情况发生。
接下来是关于LLM的逻辑怎么写其实取决于你调用的是什么LLM API因为各家的API格式不完全一样。
不过好在langGraph已经在底层帮忙把一些主流供应商的API包装了一下让你可以用**“非常简单可读的语言”**去调用他们不用担心解析API回答的问题。
但是遇到langGraph没有包装过或者是供应商对langGraph语法适配不够好的时候你需要自己去写解析逻辑。
这里我用Gemini API来举例子。
可以import针对于Gemini API定制的包。
初始化LLM接下来最重要的一步是使用with_structured_output函数来形成一个返回结构化回答的LLM实例。
记得我们定义的输出数据结构proposalOutput吗就是放在这里传进去的with_structured_output函数是langGraph提供的它的本质是把你已经定义的数据结构的要求通过prompt注入给LLM要求它遵循这个结构。
并且利用Pydantic的“运行时检查”特性检查LLM是否返回了正确的数据结构如果没有则报错。
为什么langGraph这种包装方式就管用跟我们自己写一个要求LLM生成json的提示词有什么不同langGraph的包装方式也不一定是百分百管用它也只是一种prompt形式的**“强烈暗示”LLM依然有可能叛逆。
****with_structured_output函数是一个“多态函数”。
**也就是说不管你调用什么供应商提供的API接口都可以使用这个with_structured_output函数但是它在底层实现上是不同的。
对于ChatGPT和Gemini这种非常成熟的AI API他们本身在训练的时候就会特别注意让LLM服从结构化输出的约束。
对于这种APIlangGraph会在底层**“假装”我们约定好的数据结构JSON schema是一个tool。
然后让AI误以为自己是要调用这个tool**——既然要调用tool那么就必须要严格按照tool定义的参数来进行输出。
这样做**对于LLM的暗示会更加强烈。
**尤其是一些专门针对tool call训练过的模型。
最后一重保险就是with_structured_output这个函数会自动从API返回的tool_calls中提取JSON字符串塞回Pydantic进行校验并实例化为Python对象。
这样如果输出错了不会无声无息让它过去会给你返回一个报错。
就形成了一套比较靠谱的结构化机制。
**不过报错并不是终点**报错之后怎么处理也需要定义这里就先不讲那么复杂了。
还有一点要记住的就是如果使用的是一些不支持with_structured_output函数的AI API这一套就会失效就需要自己想办法去引导AI进行结构化输出了。
message处理继续回到Node函数上面来现在我们有了可以结构化输出的LLM实例structured_llm我们需要传递给TA消息这样TA才可以回答我们。
除了刚刚已经设定过的“系统提示词”之外还需要把**“消息列表”**传递给LLM也就是储存在State里面的messages字段。
为什么这里选择传递整个messages字段呢因为可能会需要LLM不断依据人类的反馈修正邮件内容所以TA需要知道**“历史消息”**。
当然历史消息可能会累计得太长以至于超出了LLM的上下文长度在真实场景中肯定会选择处理得更加精细一点比如使用最后几条消息或者是加一个“
总结”Node。
这里就选择最简单粗暴的办法把历史消息全部传递过去。
现在我们需要传递给LLM的是final_messages。
调用invoke函数就可以拿到LLM的回答这个回答就是我们要求的proposerOutput类实例。
这里还涉及到对用户的“消息展示”我们没有前端界面就直接让Node把消息给print出来Node的输出到了return的环节这个Node要输出的是什么我们在前面分析过这个Node只需要return每个字段**“增量”的内容**就可以了。
langGraph会依据“归并函数”把“增量”更新到对应的State字段上面。
由于这个AI需要依据人类的反馈不断修正自己给出的邮件草稿draftTA肯定是需要看到人类与自己的历史聊天**我们需要把AI的回答也加入到messages字段里面。
**剩下的字段就单独return。
最后的结果是这样第四步定义Edges我们知道这个Graph里面只有一个我们自定义的Node但是由于要依据人类的反馈不断修正邮件这个Graph里面必定存在一个循环和条件边。
只有这样才能够实现不满足条件就回到emailDraft满足条件就去END。
现在来定义这个条件边的Router函数Router函数的判断规则为检查State的isConfirmed字段如果是True则走到END如果是False则返回到emailDraft。
第五步定义图把上面的信息整合起来可以定义一个有循环的图。
第六步定义main函数在这一步里面我们需要定义主程序主程序是一个**“循环”**除非用户打断或者调用达到了门槛值防止无限调用否则一直触发Graph。
初始化State放在主程序逻辑里面最后运行的结果为下面这个样子。
可以看见这个邮件起草Bot是能够记忆历史对话并且依据用户的反馈不断修改的。
总结Node就是一个**“加工站点”它加工的内容是State。
归功于langGraph的包装我们只需要返回需要更新的“增量内容”**langGraph会帮我们依据“归并函数”追加到State上面去。
我们一般遵循“**单一职责”**原则来设定Node这样做有助于提高Node的复用性。
Edge是连接“加工站点”的**“传送带”分为两种类型Normal Edge和Conditional Edge**。
Normal Edge储存的是**“不变的映射”A结束就去B。
Conditional Edge储存的是“按照条件判断的映射”**A结束之后C条件满足就去B不满足就去D。
为了逻辑上解耦Edge这个“导航”依据的是Node的**“字符串名字”**做为参照不可以直接写function name。
利用State、Nodes和Edges这三大要素可以组成一个GraphGraph需要编译compile这一步是在进行解析“字符串”等准备工作。
调用Graph的时候用invoke函数。
利用Pydantic这个精密收纳盒我们可以把不可控的LLM输出转变成严谨的Python对象。
通过with_structured_output这种“假装调工具”的强暗示让LLM输出我们想要的JSON格式。
我们搞出了一个会记忆历史对话、会听取人类反馈并不断迭代的“邮件起草Bot”。
虽然在Node里直接用input()有点“大逆不道”但它确实让我们看清了人类反馈是如何进入循环的。
最后我在一线科技企业深耕十二载见证过太多因技术更迭而跃迁的案例。
那些率先拥抱 AI 的同事早已在效率与薪资上形成代际优势我意识到有很多经验和知识值得分享给大家也可以通过我们的能力和经验解答大家在大模型的学习中的很多困惑。
我整理出这套 AI 大模型突围资料包✅AI大模型学习路线图✅Agent行业报告✅100集大模型视频教程✅大模型书籍PDF✅DeepSeek教程✅AI产品经理入门资料完整的大模型学习和面试资料已经上传带到CSDN的官方了有需要的朋友可以扫描下方二维码免费领取【保证100%免费】为什么说现在普通人就业/升职加薪的首选是AI大模型人工智能技术的爆发式增长正以不可逆转之势重塑就业市场版图。
从DeepSeek等国产大模型引发的科技圈热议到全国两会关于AI产业发展的政策聚焦再到招聘会上排起的长队AI的热度已从技术领域渗透到就业市场的每一个角落。
智联招聘的最新数据给出了最直观的印证2025年2月AI领域求职人数同比增幅突破200%远超其他行业平均水平整个人工智能行业的求职增速达到
3
4%位居各行业榜首其中人工智能工程师岗位的求职热度更是飙升
6
6%。
AI产业的快速扩张也让人才供需矛盾愈发突出。
麦肯锡报告明确预测到2030年中国AI专业人才需求将达600万人人才缺口可能高达400万人这一缺口不仅存在于核心技术领域更蔓延至产业应用的各个环节。
资料包有什么①从入门到精通的全套视频教程⑤⑥包含提示词工程、RAG、Agent等技术点② AI大模型学习路线图还有视频解说全过程AI大模型学习路线③学习电子书籍和技术文档市面上的大模型书籍确实太多了这些是我精选出来的④各大厂大模型面试题目详解⑤ 这些资料真的有用吗?这份资料由我和鲁为民博士共同整理鲁为民博士先后获得了北京清华大学学士和美国加州理工学院博士学位在包括IEEE Transactions等学术期刊和诸多国际会议上发表了超过50篇学术论文、取得了多项美国和中国发明专利同时还斩获了吴文俊人工智能科学技术奖。
目前我正在和鲁博士共同进行人工智能的研究。
所有的视频教程由智泊AI老师录制且资料与智泊AI共享相互补充。
这份学习大礼包应该算是现在最全面的大模型学习资料了。
资料内容涵盖了从入门到进阶的各类视频教程和实战项目无论你是小白还是有些技术基础的这份资料都绝对能帮助你提升薪资待遇转行大模型岗位。
智泊AI始终秉持着“让每个人平等享受到优质教育资源”的育人理念通过动态追踪大模型开发、数据标注伦理等前沿技术趋势构建起前沿课程智能实训精准就业的高效培养体系。
课堂上不光教理论还带着学员做了十多个真实项目。
学员要亲自上手搞数据清洗、模型调优这些硬核操作把课本知识变成真本事如果说你是以下人群中的其中一类都可以来智泊AI学习人工智能找到高薪工作一次小小的“投资”换来的是终身受益应届毕业生无工作经验但想要系统学习AI大模型技术期待通过实战项目掌握核心技术。
零基础转型非技术背景但关注AI应用场景计划通过低代码工具实现“AI行业”跨界。
业务赋能 突破瓶颈传统开发者Java/前端等学习Transformer架构与LangChain框架向AI全栈工程师转型。
获取方式有需要的小伙伴可以保存图片到wx扫描二v码免费领取【保证100%免费】**