生成式人工智能与 LangChain 第二版(一)
随着大型语言模型LLMs) 现在为从客户服务聊天机器人到复杂的代码生成系统的一切提供动力,生成式 AI 已迅速从研究实验室的奇思妙想转变为生产工作马。然而,实验原型和生产就绪 AI 应用之间存在一个显著的差距。根据行业研究,尽管对生成式 AI 的热情很高,但超过 30% 的项目因可靠性问题、评估复杂性和集成挑战而未能超越概念验证。LangChain 框架已成为跨越这一鸿沟的关键桥梁,为开发者提供了
原文:
zh.annas-archive.org/md5/09fa42dfe7b300a9759b9ebf99906d4c
译者:飞龙
使用 LangChain 进行生成式 AI
第二版
使用 Python、LangChain 和 LangGraph 构建生产就绪的 LLM 应用程序和高级代理
本·奥夫阿特
列昂尼德·库利金
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/New_Packt_Logo.png
使用 LangChain 进行生成式 AI
第二版
版权所有 © 2025 Packt Publishing
版权所有。未经出版者事先书面许可,本书的任何部分不得以任何形式或通过任何手段进行复制、存储在检索系统中或以任何方式传播,除非在评论或评论中嵌入的简短引用。
在准备本书的过程中,已尽一切努力确保所提供信息的准确性。然而,本书中的信息销售不附带任何明示或暗示的保证。作者、Packt Publishing 或其经销商和分销商不对由此书直接或间接造成的任何损害承担责任。
Packt Publishing 尽力通过适当使用大写字母提供本书中提到的所有公司和产品的商标信息。然而,Packt Publishing 不能保证此信息的准确性。
投资组合总监: Gebin George
关系负责人: Ali Abidi
项目经理: Prajakta Naik
内容工程师: Tanya D’cruz
技术编辑: Irfa Ansari
编辑: Safis Editing
索引者: Manju Arasan
校对: Tanya D’cruz
生产设计师: Ajay Patule
增长负责人: Nimisha Dua
首次出版:2023 年 12 月
第二版:2025 年 5 月
生产参考:1190525
由 Packt Publishing Ltd. 出版
格罗斯文诺大厦
11 圣保罗广场
伯明翰
B3 1RB,英国。
ISBN 978-1-83702-201-4
致在我一生中指导我的人——特别是托尼·林德贝格,他的个人正直和毅力是我巨大的灵感源泉——以及我的儿子尼古拉斯和我的伴侣黛安。
——本·奥夫阿特
致我的妻子克谢尼亚,她坚定不移的爱和乐观一直是我多年来的坚定支持;致我的岳母塔蒂亚娜,她对我的信念——即使在我最疯狂的努力中——是一种难以置信的力量源泉;以及我的孩子们,马特维和米莱娜:我希望你们有一天能读到它。
——列昂尼德·库利金
第一章:贡献者
关于作者
本·奥夫阿特博士,拥有超过 15 年的工作经验,是一位 AI 实施专家。作为切尔西 AI 风险投资公司的创始人,他专注于帮助小型和中型企业实施能够带来切实回报的企业级 AI 解决方案。他的系统已经防止了数百万的欺诈损失,并以低于 300 毫秒的延迟处理交易。凭借计算神经科学的背景,本为实用 AI 应用带来了罕见的深度——从超级计算机脑模型到结合技术卓越与商业战略的生产系统。
首先和最重要的是,我要感谢我的合著者,Leo——一位超级程序员,他在整个过程中都表现出了耐心,并在需要建议时总是随时准备着。如果没有 Packt 的同事们,这本书也不会成为现在这个样子,特别是我们的编辑 Tanya,她总是在需要时提供洞察力和鼓励的话语。最后,审稿人们非常乐于助人,他们的批评非常慷慨,确保我们没有遗漏任何内容。任何剩余的错误或疏忽都是完全我的责任。
Leonid Kuligin 是谷歌云的一名员工 AI 工程师,专注于生成式 AI 和经典机器学习解决方案,如需求预测和优化问题。Leonid 是 LangChain 在谷歌云集成方面的关键维护者,并在 CDTM(慕尼黑工业大学和慕尼黑大学联合机构)担任客座讲师。在谷歌之前,Leonid 在德国、俄罗斯和美国的科技、金融和零售公司积累了超过 20 年的基于复杂机器学习和数据处理解决方案(如搜索、地图和投资管理)构建 B2C 和 B2B 应用的经验。
我想要向所有在谷歌工作的同事们表达我真诚的感激之情,与他们一起工作是一种乐趣和快乐,他们在本书的创作以及许多其他事业中支持了我。特别感谢 Max Tschochohei、Lucio Floretta 和 Thomas Cliett。我的感激之情也延伸到整个 LangChain 社区,特别是 Harrison Chase,他持续开发 LangChain 框架,使我的工程师工作大大简化。
关于审稿人
Max Tschochohei 为企业客户提供如何在谷歌云实现他们的 AI 和 ML 雄心的建议。作为谷歌云咨询的工程经理,他领导着 AI 工程师团队在关键客户项目上工作。虽然他的工作涵盖了谷歌云产品组合中所有 AI 产品和解决方案的全范围,但他特别对代理系统、机器学习运营和 AI 在医疗保健中的应用感兴趣。在慕尼黑加入谷歌之前,Max 在 KPMG 和波士顿咨询集团担任了多年的顾问,他还领导了新加坡政府组织 NTUC Enterprise 的数字化转型。Max 持有来自考文垂大学的经济学博士学位。
Rany ElHousieny 是一位拥有超过二十年在 AI、NLP 和 ML 领域经验的 AI 解决方案架构师和 AI 工程经理。在他的职业生涯中,他专注于 AI 模型的开发和部署,撰写了多篇关于 AI 系统架构和道德 AI 部署的文章。他在微软等公司领导了开创性的项目,在那里他推动了 NLP 和语言理解智能服务(LUIS)的进步。目前,他在 Clearwater Analytics 公司扮演着关键角色,推动生成式 AI 和 AI 驱动的金融和投资管理解决方案的创新。
尼古拉斯·比埃弗(Nicolas Bievre)是 Meta 的机器学习工程师,在 AI、推荐系统、LLMs 和生成式 AI 方面拥有丰富的经验,这些应用领域包括广告和医疗保健。他在 Meta 和 PayPal 担任过关键的 AI 领导角色,设计和实施用于为上亿用户个性化内容的大型推荐系统。他毕业于斯坦福大学,在该校发表了被同行评审的 AI 和生物信息学领域的领先期刊的研究成果。尼古拉斯因其在国际上的贡献而获得认可,获得了“核心广告增长隐私”奖和“海外杰出人才”奖等荣誉。他还担任法国政府的 AI 咨询师,以及顶级 AI 组织的审稿人。
加入我们的 Discord 和 Reddit 社区
对本书有任何疑问或想参与关于生成式 AI 和 LLMs 的讨论?加入我们的 Discord 服务器,网址为 packt.link/4Bbd9
,以及我们的 Reddit 频道 packt.link/wcYOQ
,与志同道合的 AI 专业人士建立联系、分享和协作。
前言
随着 大型语言模型 (LLMs) 现在为从客户服务聊天机器人到复杂的代码生成系统的一切提供动力,生成式 AI 已迅速从研究实验室的奇思妙想转变为生产工作马。然而,实验原型和生产就绪 AI 应用之间存在一个显著的差距。根据行业研究,尽管对生成式 AI 的热情很高,但超过 30% 的项目因可靠性问题、评估复杂性和集成挑战而未能超越概念验证。LangChain 框架已成为跨越这一鸿沟的关键桥梁,为开发者提供了构建稳健、可扩展和实用 LLM 应用的工具。
本书旨在帮助您缩小这一差距。它是您构建在生产环境中真正起作用的 LLM 应用的实用指南。我们关注大多数生成式 AI 项目会遭遇的现实问题:输出不一致、调试困难、脆弱的工具集成和扩展瓶颈。通过使用 LangChain、LangGraph 和不断增长的生成式 AI 生态系统中的其他工具的动手示例和经过测试的模式,您将学会构建您的组织可以自信部署和维护以解决实际问题的系统。
本书面向对象
本书主要面向具有基本 Python 知识的软件开发人员,他们希望使用 LLMs 构建生产就绪的应用程序。您不需要广泛的机器学习专业知识,但一些对 AI 概念的了解将帮助您更快地通过材料。到本书结束时,您将能够自信地实施需要专门 AI 知识的其他高级 LLM 架构。
如果您是一位正在转向 LLM 应用开发的数据科学家,您会发现实际的实施模式特别有价值,因为它们弥合了实验笔记本和可部署系统之间的差距。本书对 RAG 实施、评估框架和可观察性实践的系统方法解决了您在尝试将有希望的原型扩展为可靠服务时可能遇到的常见挫折。
对于在其组织中评估 LLM 技术的技术决策者,本书提供了关于成功 LLM 项目实施的策略洞察。您将了解区分实验系统与生产就绪系统的架构模式,学习识别高价值用例,并发现如何避免导致大多数项目失败的集成和扩展问题。本书提供了评估实施方法和做出明智技术决策的明确标准。
本书涵盖内容
第一章,生成式 AI 的崛起:从语言模型到智能体,介绍了现代 LLM 的景观,并将 LangChain 定位为构建生产就绪 AI 应用的框架。你将了解基本 LLM 的实用局限性以及像 LangChain 这样的框架如何帮助标准化和克服这些挑战。这个基础将帮助你就针对特定用例实施哪些智能体技术做出明智的决定。
第二章,LangChain 的入门步骤,通过实际动手示例让你立即开始构建。你将设置合适的发展环境,理解 LangChain 的核心组件(模型接口、提示、模板和 LCEL),并创建简单的链。本章展示了如何运行基于云和本地的模型,根据项目需求,为你提供了平衡成本、隐私和性能的选项。你还将探索结合文本和视觉理解的简单多模态应用。这些基础知识为日益复杂的 AI 应用提供了构建块。
第三章,使用 LangGraph 构建工作流程,深入探讨了使用 LangChain 和 LangGraph 创建复杂工作流程。你将学习如何使用节点和边构建工作流程,包括基于状态的分支条件边。本章涵盖了输出解析、错误处理、提示工程技术(零样本和动态少量样本提示)以及使用 Map-Reduce 模式处理长上下文。你还将实现用于管理聊天历史的内存机制。这些技能解决了为什么许多 LLM 应用在现实条件下失败的原因,并为你提供了构建可靠性能系统的工具。
第四章,构建智能 RAG 系统,通过将 LLM 建立在可靠的外部知识上解决了“幻觉问题”。你将掌握向量存储、文档处理和检索策略,这些策略可以提高响应准确性。本章的企业级文档聊天机器人项目展示了如何实施保持一致性和合规性的企业级 RAG 管道——这一能力直接针对行业调查中提到的数据质量问题。故障排除部分涵盖了七个常见的 RAG 故障点,并为每个故障点提供了实用的解决方案。
第五章,构建智能体,探讨了工具使用脆弱性问题——这是智能体自主性的核心瓶颈。你将实现 ReACT 模式来提高智能体的推理和决策能力,开发稳健的定制工具,并构建容错性工具调用流程。通过生成结构化输出和构建研究智能体的实际示例,你将了解智能体是什么,并使用 LangGraph 实现你的第一个计划-求解智能体,为更高级的智能体架构奠定基础。
第六章,高级应用和多智能体系统,涵盖了用于智能体 AI 应用的架构模式。你将探索多智能体架构以及组织智能体之间通信的方法,实现一个具有自我反思能力的先进智能体,该智能体使用工具来回答复杂问题。本章还涵盖了 LangGraph 流处理、高级控制流、闭环中的人类自适应系统以及思维树模式。你将了解 LangChain 和 LangGraph 中的记忆机制,包括缓存和存储,这将使你能够创建能够处理单智能体方法无法应对的复杂问题的系统——这是生产就绪系统的一项关键能力。
第七章,软件开发和数据分析智能体,展示了自然语言如何成为编程和数据分析的有力接口。你将实现基于 LLM 的代码生成、使用 RAG 进行代码检索和文档搜索的解决方案。这些示例展示了如何将 LLM 智能体集成到现有的开发和数据工作流程中,说明了它们如何补充而不是取代传统的编程技能。
第八章,评估和测试,概述了在生产部署前评估 LLM 应用的方法。你将了解系统级评估、评估驱动设计以及离线和在线方法。本章提供了使用精确匹配和 LLM 作为裁判的评估方法来实现正确性评估的实用示例,并展示了 LangSmith 等工具进行综合测试和监控。这些技术直接提高了可靠性,并有助于证明你的 LLM 应用的商业价值。
第九章,可观察性和生产部署,提供了将 LLM 应用程序部署到生产的指南,重点关注系统设计、扩展策略、监控和确保高可用性。本章涵盖了针对 LLM 的日志记录、API 设计、成本优化和冗余策略。您将探索模型上下文协议(MCP)并学习如何实施解决部署生成式 AI 系统独特挑战的可观察性实践。本章中的实际部署模式有助于您避免许多 LLM 项目无法达到生产阶段的常见陷阱。
第十章,LLM 应用的未来,展望了生成式 AI 中出现的趋势、演变的架构和伦理考量。本章探讨了新技术、市场发展、潜在的社会影响和负责任开发的指南。您将深入了解该领域可能如何发展,以及如何定位您的技能和应用以适应未来的进步,完成从基本 LLM 理解到构建和部署生产就绪、未来证明的 AI 系统的旅程。
为了充分利用这本书
在深入之前,确保您有一些事情准备好以充分利用您的学习体验是有帮助的。这本书旨在实用和动手操作,因此拥有正确的环境、工具和心态将帮助您顺利跟随并从每一章中获得最大价值。以下是我们的建议:
-
环境要求:在 Windows、macOS 或 Linux 等任何主要操作系统上设置一个 Python 3.10+的开发环境。所有代码示例都是跨平台兼容的,并且经过彻底测试。
-
API 访问(可选但推荐):虽然我们展示了使用可以在本地运行的开源模型,但访问像 OpenAI、Anthropic 或其他 LLM 提供商这样的商业 API 提供商将允许您使用更强大的模型。许多示例包括本地和基于 API 的方法,因此您可以根据您的预算和性能需求进行选择。
-
学习方法:我们建议您亲自输入代码而不是复制粘贴。这种动手实践强化了学习并鼓励实验。每一章都是基于之前引入的概念,因此按顺序完成它们将为您打下最坚实的基础。
-
背景知识:需要基本的 Python 熟练度,但不需要机器学习或 LLM 的先前经验。我们会在适当的时候解释关键概念。如果您已经熟悉 LLM,您可以专注于区分这本书的实现模式和部署就绪方面的内容。
书中涵盖的软件/硬件 Python 3.10+ LangChain 0.3.1+ LangGraph 0.2.10+ 不同的 LLM 提供商(Anthropic、Google、OpenAI、本地模型)
您将在第一章中找到有关环境设置的详细指南,以及清晰的解释和逐步说明,以帮助您开始。鉴于 LangChain、LangGraph 和更广泛生态系统的快速变化性质,我们强烈建议遵循这些设置步骤——跳过这些步骤可能会导致未来出现可避免的问题。
下载示例代码文件
本书代码包托管在 GitHub 上,网址为github.com/benman1/generative_ai_with_langchain
。我们建议您在阅读章节时自行输入代码或使用存储库。如果代码有更新,它将在 GitHub 存储库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing
找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781837022014
。
使用的约定
本书使用了多种文本约定。
CodeInText
:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“让我们也从thread-a
的初始检查点恢复。我们会看到我们从一个空的历史记录开始:”
代码块设置如下:
checkpoint_id = checkpoints[-1].config["configurable"]["checkpoint_id"]
_ = graph.invoke(
[HumanMessage(content="test")],
config={"configurable": {"thread_id": "thread-a", "checkpoint_id": checkpoint_id}})
任何命令行输入或输出都应如下所示:
$ pip install langchain langchain-openai
粗体:表示新术语、重要词汇或屏幕上出现的词汇。例如,菜单或对话框中的文字会以这种方式显示。例如:“谷歌研究团队在 2022 年初引入了思维链(CoT)技术。”
警告或重要提示如下所示。
小贴士和技巧如下所示。
联系我们
订阅 AI_Distilled,这是 AI 专业人士、研究人员和创新者的首选通讯简报,
欢迎读者反馈。
如果您发现任何错误或有建议,请通过 GitHub 问题、discord 聊天或 Packt 网站上的勘误表单报告,最好是通过 GitHub 问题报告。
关于 GitHub 上的问题,请参阅github.com/benman1/generative_ai_with_langchain/issues
。
如果您对本书的内容或定制项目有任何疑问,请随时通过ben@chelseaai.co.uk
联系我们。
一般反馈:请发送电子邮件至feedback@packtpub.com
,并在邮件主题中提及本书的标题。如果您对本书的任何方面有疑问,请发送电子邮件至questions@packtpub.com
。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/submit-errata
,点击提交勘误,并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将非常感谢。请通过copyright@packtpub.com
与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com/
。
分享您的想法
一旦您阅读了《使用 LangChain 的生成式 AI,第二版》,我们非常乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在旅途中阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何时间、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的邮箱。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接:
packt.link/free-ebook/9781837022014
-
提交您的购买证明。
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱。
第二章:生成式 AI 的崛起:从语言模型到智能体
实验性和生产就绪智能体之间的差距非常明显。根据 LangChain 的智能体状态报告,性能质量是 51%使用智能体的公司最关心的问题,但只有 39.8%的公司实施了适当的评估系统。我们的书籍从两个前沿领域弥合了这一差距:首先,通过展示 LangChain 和 LangSmith 如何提供强大的测试和可观察性解决方案;其次,通过展示 LangGraph 的状态管理如何使复杂、可靠的智能体系统成为可能。您将找到经过生产测试的代码模式,这些模式利用每个工具的优势,以企业规模实施,并将基本的 RAG 扩展到强大的知识系统。
LangChain 通过提供现成的构建块、统一的供应商 API 和详细的教程,加速了产品的上市时间。此外,LangChain 和 LangSmith 的调试和跟踪功能简化了复杂智能体行为的分析。最后,LangGraph 在执行其智能体 AI 背后的哲学方面表现出色——它允许开发者对工作流程中的大型语言模型(LLM)进行部分控制流(以及管理 LLM 应拥有的控制级别),同时仍然使智能体工作流程可靠且性能良好。
在本章中,我们将探讨 LLM 如何演变成智能体 AI 系统的基石,以及像 LangChain 和 LangGraph 这样的框架如何将这些模型转化为生产就绪的应用。我们还将检查现代 LLM 的格局,了解原始 LLM 的限制,并介绍构成本书中我们将要解决的手动开发基础的智能体应用的核心概念。
简而言之,本书将涵盖以下主题:
-
现代 LLM 格局
-
从模型到智能体应用
-
介绍 LangChain
现代 LLM 格局
人工智能(AI)长期以来一直是人们着迷和研究的话题,但最近在生成式 AI 方面的进步已经推动了其主流的采用。与传统的 AI 系统不同,这些系统对数据进行分类或做出预测,生成式 AI 可以通过利用大量的训练数据来创建新的内容——文本、图像、代码等等。
生成式 AI 革命是由 2017 年引入的 transformer 架构所催化的,它使模型能够以前所未有的对上下文和关系的理解来处理文本。随着研究人员将这些模型从数百万参数扩展到数十亿参数,他们发现了一些令人瞩目的事情:更大的模型不仅仅是渐进式地更好——它们还表现出全新的涌现能力,如少样本学习、复杂推理和创造性生成,这些能力并非明确编程。最终,2022 年 ChatGPT 的发布标志着转折点,向公众展示了这些能力,并引发了广泛的应用。
在 Llama 和 Mistral 等模型引领的开源革命中,格局再次发生转变,将强大的 AI 访问权民主化,超越了主要科技公司。然而,这些高级功能伴随着重大的局限性——模型无法可靠地使用工具,通过复杂问题进行推理,或在交互过程中保持情境。这种原始模型力量和实际效用之间的差距产生了对像 LangChain 这样的专用框架的需求,这些框架将这些模型从令人印象深刻的文本生成器转变为功能齐全、生产就绪的代理,能够解决现实世界的问题。
关键术语
工具:AI 模型可以用来与世界交互的外部实用程序或函数。工具允许代理执行搜索网络、计算值或访问数据库等操作,以克服 LLMs 固有的局限性。
记忆:允许 AI 应用在交互过程中存储和检索信息的系统。通过跟踪之前的输入、输出和重要信息,记忆使对话和复杂工作流程具有情境意识。
基于人类反馈的强化学习(RLHF):一种训练技术,其中 AI 模型从直接的人类反馈中学习,优化其性能以符合人类偏好。RLHF 有助于创建更帮助性、更安全且与人类价值观一致的模型。
代理:能够感知其环境、做出决策并采取行动以实现目标的 AI 系统。在 LangChain 中,代理使用 LLMs 来解释任务、选择适当的工具,并在最小化人工干预的情况下执行多步骤过程。
年 | 发展 | 关键特性 |
---|---|---|
1990 年代 | IBM 对齐模型 | 统计机器翻译 |
2000 年代 | 网络规模数据集 | 大规模统计模型 |
2009 | 统计模型主导 | 大规模文本摄入 |
2012 | 深度学习获得动力 | 神经网络优于统计模型 |
2016 | 神经机器翻译(NMT) | Seq2seq 深度 LSTMs 取代统计方法 |
2017 | Transformer 架构 | 自注意力革命性地改变了 NLP |
2018 | BERT 和 GPT-1 | 基于 Transformer 的语言理解和生成 |
2019 | GPT-2 | 大规模文本生成,公众意识提高 |
2020 | GPT-3 | 基于 API 的访问,最先进的性能 |
2022 | ChatGPT | LLMs 的广泛应用 |
2023 | 大型多模态模型(LMMs) | AI 模型处理文本、图像和音频 |
|
2024
OpenAI o1 | 更强的推理能力 |
---|---|
2025 | DeepSeek R1 |
表 1.1:语言模型主要发展的时间线
LLMs 领域正在迅速发展,多个模型在性能、能力和可访问性方面展开竞争。每个提供商都带来独特的优势,从 OpenAI 的高级通用人工智能到 Mistral 的开源、高效模型。了解这些模型之间的差异有助于实践者在将 LLMs 集成到其应用程序时做出明智的决定。
模型比较
以下要点概述了比较不同 LLMs 时需要考虑的关键因素,重点关注其可访问性、规模、能力和专业化:
-
开源模型与闭源模型:开源模型如 Mistral 和 LLaMA 提供透明度和本地运行的能力,而闭源模型如 GPT-4 和 Claude 则可以通过 API 访问。开源 LLMs 可以被下载和修改,使开发人员和研究人员能够调查和基于其架构进行构建,尽管可能适用特定的使用条款。
-
规模和能力:较大的模型通常提供更好的性能,但需要更多的计算资源。这使得较小的模型非常适合在计算能力或内存有限的设备上使用,并且使用成本可以显著降低。小型语言模型(SLMs)的参数数量相对较少,通常使用数百万到数十亿个参数,而大型语言模型(LLMs)可以拥有数百亿甚至数千亿的参数。
-
专用模型:一些大型语言模型(LLMs)针对特定任务进行了优化,例如代码生成(例如,Codex)或数学推理(例如,Minerva)。
语言模型规模的增加是它们令人印象深刻的性能提升的主要驱动力。然而,最近在架构和训练方法上出现的变化导致了在性能方面的参数效率的提高。
模型缩放定律
经验推导的缩放定律根据给定的训练预算、数据集大小和参数数量预测 LLMs 的性能。如果这是真的,这意味着高度强大的系统将集中在大型科技公司手中,然而,我们在最近几个月看到了显著的转变。
Kaplan 等人提出的KM 缩放定律,通过经验分析和拟合模型性能与不同数据大小、模型大小和训练计算之间的关系,呈现幂律关系,表明模型性能与模型大小、数据集大小和训练计算等因素之间存在强烈的相互依赖性。
Google DeepMind 团队提出的Chinchilla 缩放定律涉及对更广泛范围的模型大小和数据大小的实验。它建议对计算预算进行最优分配,以适应模型大小和数据大小,这可以通过在约束下优化特定的损失函数来确定。
然而,未来的进步可能更多地取决于模型架构、数据清洗和模型算法创新,而不是单纯的大小。例如,phi 模型,首次在*《教科书都是你需要的一切》*(2023 年,Gunasekar 等人)中提出,大约有 10 亿个参数,表明模型即使规模较小,也能在评估基准上实现高精度。作者建议提高数据质量可以显著改变扩展定律的形状。
此外,还有关于简化模型架构的研究,这些模型具有显著更少的参数,并且仅略微降低精度(例如,只需要一个宽前馈网络,Pessoa Pires 等人,2023 年)。此外,微调、量化、蒸馏和提示技术等技术可以使较小的模型利用大型基础模型的能力,而无需复制其成本。为了弥补模型限制,搜索引擎和计算器等工具已被纳入代理中,多步推理策略、插件和扩展可能越来越多地被用来扩展功能。
未来可能会看到大型通用模型与较小且更易于访问的模型的共存,这些模型提供更快、更便宜的培训、维护和推理。
让我们讨论一下各种 LLM 的比较概述,突出它们的关键特性和差异化因素。我们将探讨开源与闭源模型、模型大小和能力以及专用模型等方面。通过了解这些区别,您可以选择最适合您特定需求和应用的 LLM。
LLM 提供商格局
您可以通过 OpenAI、谷歌和 Anthropic 等主要提供商的网站或 API 访问 LLM,以及其他越来越多的提供商。随着对 LLM 的需求增长,许多提供商已进入该领域,每个都提供具有独特功能和权衡的模型。开发者需要了解可用于将强大模型集成到其应用程序中的各种访问选项。提供商的选择将显著影响开发体验、性能特征和运营成本。
下表提供了领先的大型语言模型(LLM)提供商及其提供的模型示例的比较概述:
提供商 | 知名模型 | 关键特性和优势 |
---|---|---|
OpenAI | GPT-4o, GPT-4.5;o1;o3-mini | 强大的通用性能,专有模型,高级推理;在实时跨文本、音频、视觉和视频中进行多模态推理 |
Anthropic | Claude 3.7 Sonnet; Claude 3.5 Haiku | 在实时响应和扩展的“思考”阶段之间切换;在编码基准测试中优于 OpenAI 的 o1 |
谷歌 | Gemini 2.5, 2.0(闪存和专业版),Gemini 1.5 | 低延迟和成本,大上下文窗口(高达 2M 个标记),多模态输入和输出,推理能力 |
Cohere | Command R,Command R Plus | 检索增强生成,企业 AI 解决方案 |
Mistral AI | Mistral Large;Mistral 7B | 开放权重,高效推理,多语言支持 |
AWS | Titan | 企业级 AI 模型,优化用于 AWS 云 |
|
DeepSeek
R1 | 以数学为先:解决奥林匹克级别的难题;成本效益高,优化用于多语言和编程任务 |
---|---|
Together AI | 运行开源模型的基础设施 |
表 1.2:主要 LLM 提供商及其用于 LangChain 实现的旗舰模型的比较概述
其他组织开发 LLM,但并不一定通过应用程序编程接口(APIs)向开发者提供。例如,Meta AI 开发了非常有影响力的 Llama 模型系列,该系列具有强大的推理和代码生成能力,并以开源许可证发布。
你可以通过 Hugging Face 或其他提供商访问一系列开源模型。你甚至可以下载这些开源模型,微调它们,或完全训练它们。我们将在第二章中实际尝试这一点。
一旦你选择了合适的模型,下一个关键步骤就是了解如何控制其行为以满足你特定的应用需求。虽然访问模型为你提供了计算能力,但生成参数的选择将把原始模型的力量转化为适用于你应用程序中不同用例的定制输出。
现在我们已经了解了 LLM 提供商的格局,让我们讨论 LLM 实施中的另一个关键方面:许可证考虑。不同模型的许可证条款在很大程度上影响了你在应用程序中使用它们的方式。
许可证
LLM 在不同的许可证模型下可用,这影响了它们在实际中的使用方式。开源模型如 Mixtral 和 BERT 可以自由使用、修改并集成到应用程序中。这些模型允许开发者本地运行它们,研究其行为,并在研究和商业目的上在此基础上构建。
相比之下,像 GPT-4 和 Claude 这样的专有模型只能通过 API 访问,其内部工作原理保持私密。虽然这确保了性能的一致性和定期更新,但也意味着依赖于外部服务,并且通常会产生使用费用。
一些模型,如 Llama 2,采取折中方案,为研究和商业用途提供宽松的许可证,同时保持某些使用条件。有关特定模型许可证及其影响的详细信息,请参阅每个模型的文档或咨询模型开放框架:isitopen.ai/
.
模型开放框架(MOF)根据诸如访问模型架构细节、训练方法及超参数、数据来源和处理信息、开发决策的文档、评估模型运作、偏见和局限性的能力、代码模块化、发布的模型卡片、可服务模型的可用性、本地运行选项、源代码可用性和再分发权利等标准评估语言模型。
通常,开源许可促进了对模型的广泛采用、协作和创新,这对研究和商业开发都有益。专有许可通常给予公司独家控制权,但可能限制学术研究进展。非商业许可通常限制商业用途,同时允许研究。
通过使知识和知识工作更加易于获取和适应,生成式 AI 模型有可能使竞争场域公平,并为各行各业的人创造新的机会。
人工智能的演变使我们达到了一个关键时刻,AI 系统不仅可以处理信息,还可以采取自主行动。下一节将探讨从基本语言模型到更复杂,最终到完全代理应用的转变。
关于 AI 模型许可提供的信息仅用于教育目的,并不构成法律建议。许可条款差异很大且发展迅速。组织应咨询合格的法律顾问,以了解其 AI 实施的具体许可决策。
从模型到代理应用
如前所述,LLMs 已经在自然语言处理中展现出非凡的流畅性。然而,尽管它们令人印象深刻,但它们仍然本质上是反应性的而不是主动性的。它们缺乏采取独立行动、有意义地与外部系统交互或自主实现复杂目标的能力。
为了解锁 AI 能力的下一阶段,我们需要超越被动的文本生成,转向代理 AI——能够规划、推理并采取行动以最小化人类干预完成任务的系统。在探索代理 AI 的潜力之前,首先了解 LLMs 的核心局限性,这些局限性是这种演变所必需的。
传统 LLMs 的局限性
尽管 LLMs 具有高级的语言能力,但它们固有的限制限制了它们在现实世界应用中的有效性:
-
缺乏真正的理解:大型语言模型(LLMs)通过根据训练数据中的统计模式预测下一个最可能出现的单词来生成类似人类的文本。然而,它们并不像人类那样理解意义。这导致幻觉——自信地将错误信息当作事实陈述——以及生成看似合理但实际上错误、误导或不合逻辑的输出。正如 Bender 等人(2021)所描述的,LLMs 作为“随机鹦鹉”——重复模式而没有真正的理解。
-
在复杂推理和问题解决上的挑战:虽然 LLMs 在检索和重新格式化知识方面表现出色,但它们在多步推理、逻辑谜题和数学问题解决上存在困难。它们通常无法将问题分解为子任务或在不同上下文中综合信息。没有像思维链推理这样的明确提示技术,它们推断或推理的能力仍然不可靠。
-
知识过时和外部访问有限:LLMs 是在静态数据集上训练的,并且没有实时访问当前事件、动态数据库或实时信息源。这使得它们不适合需要最新知识的任务,例如财务分析、突发新闻摘要或需要最新发现的科学研究。
-
没有原生的工具使用或行动能力:LLMs 在独立状态下运行——它们无法与 API 交互、检索实时数据、执行代码或修改外部系统。这种缺乏工具集成使得它们在需要现实世界行动的场景中效果较差,例如进行网络搜索、自动化工作流程或控制软件系统。
-
偏见、伦理担忧和可靠性问题:由于 LLMs 从可能包含偏见的庞大数据集中学习,它们可能会无意中加强意识形态、社会或文化偏见。重要的是,即使对于开源模型,对于大多数从业者来说,访问和审计完整训练数据以识别和减轻这些偏见仍然具有挑战性。此外,它们可能会在没有理解其输出伦理影响的情况下生成误导性或有害信息。
-
计算成本和效率挑战:大规模部署和运行 LLMs 需要大量的计算资源,这使得它们成本高昂且能耗密集。更大的模型也可能引入延迟,减慢实时应用的响应时间。
为了克服这些限制,AI 系统必须从被动的文本生成器进化为能够规划、推理并与环境交互的主动代理。这正是代理 AI 发挥作用的地方——将 LLMs 与工具使用、决策机制和自主执行能力集成,以增强其功能。
虽然像 LangChain 这样的框架为 LLMs 的局限性提供了全面的解决方案,但理解基本的提示工程技术仍然很有价值。像少样本学习、思维链和结构化提示这样的方法可以显著提高模型在特定任务上的性能。第三章将详细介绍这些技术,展示 LangChain 如何帮助标准化和优化提示模式,同时最大限度地减少在每个应用中需要定制提示工程的需求。
下一节将探讨代理 AI 如何扩展传统 LLMs 的功能,并为自动化、问题解决和智能决策解锁新的可能性。
理解 LLM 应用
LLM 应用代表了原始模型能力与实际商业价值之间的桥梁。虽然 LLM 拥有令人印象深刻的语言处理能力,但它们需要深思熟虑的整合才能提供现实世界的解决方案。这些应用大致分为两大类:复杂集成应用和自主代理。
复杂集成应用通过将大型语言模型(LLM)整合到现有流程中,增强了人类工作流程,包括:
-
提供分析和建议的决策支持系统
-
具有人类审查的内容生成管道
-
增强人类能力的交互式工具
-
在人类监督下的工作流程自动化
自主代理在最小的人为干预下运行,通过 LLM 的整合进一步增强了工作流程。例如:
-
执行定义工作流程的任务自动化代理
-
信息收集和分析系统
-
用于复杂任务协调的多代理系统
LangChain 为集成应用和自主代理提供框架,提供灵活的组件,支持各种架构选择。本书将探讨这两种方法,展示如何构建符合您特定要求的可靠、生产就绪的系统。
代理的自主系统可能非常强大,因此值得进一步探索。
理解 AI 代理
有时人们开玩笑说 AI 只是 ML 的华丽辞藻,或者 AI 是穿着西装的 ML,如图所示;然而,这背后还有更多内容,我们将看到。
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_01_01.png
图 1.1:穿着西装的 ML。由 replicate.com 上的模型生成,Diffusers Stable Diffusion v2.1
一个 AI 代理代表了从原始认知能力到实际行动的桥梁。虽然 LLM 拥有庞大的知识和处理能力,但它仍然缺乏主动性,本质上仍然是反应性的。AI 代理通过结构化的工作流程将这种被动能力转化为主动效用,这些工作流程解析需求、分析选项并执行行动。
代理式 AI 使自主系统能够在最小的人为干预下做出决策和独立行动。与遵循固定规则的确定性系统不同,代理式 AI 依赖于模式和可能性来做出明智的选择。它通过一个称为代理的自主软件组件网络来运行,这些代理从用户行为和大量数据集中学习,以随着时间的推移不断改进。
AI 中的代理指的是系统独立行动以实现目标的能力。真正的代理意味着 AI 系统可以通过学习交互和反馈来感知其环境、做出决策、行动并适应。原始 AI 与代理之间的区别与知识和专业知识之间的区别相似。考虑一位理解复杂理论的杰出研究人员,但在实际应用上却遇到困难。代理系统增加了有目的行动的关键要素,将抽象能力转化为具体成果。
在 LLM 的背景下,代理 AI 涉及开发能够自主行动、理解情境、适应新信息并与人类协作解决复杂挑战的系统。这些 AI 代理利用 LLM 来处理信息、生成响应并根据定义的目标执行任务。
尤其是 AI 代理通过整合记忆、工具使用和决策框架来扩展 LLM 的能力。这些代理可以:
-
在交互中保留和回忆信息。
-
利用外部工具、API 和数据库。
-
规划和执行多步骤工作流程。
代理的价值在于减少对持续人类监督的需求。而不是为每个请求手动提示 LLM,代理可以主动执行任务,对新数据进行反应,并与现实世界应用集成。
AI 代理是代表用户行动的系统,利用 LLM 以及外部工具、记忆和决策框架。AI 代理背后的希望是它们可以自动化复杂的工作流程,减少人力,同时提高效率和准确性。通过允许系统自主行动,代理承诺在 AI 驱动应用中解锁新的自动化水平。但这些希望是合理的吗?
尽管它们具有潜力,但 AI 代理面临着重大的挑战:
-
可靠性:确保代理在无监督的情况下做出正确、情境感知的决策是困难的。
-
泛化:许多代理在狭窄领域表现良好,但在开放性、多领域任务上却遇到困难。
-
缺乏信任:用户必须相信代理将负责任地行动,避免意外行为,并尊重隐私限制。
-
协调复杂性:多代理系统在协作执行任务时往往效率低下,存在沟通不畅的问题。
适用于生产的代理系统必须解决不仅仅是理论上的挑战,还包括实际实施障碍,如:
-
速率限制和 API 配额
-
令牌上下文溢出错误
-
幻觉管理
-
成本优化
LangChain 和 LangSmith 为这些挑战提供了稳健的解决方案,我们将在第八章和第九章中深入探讨。这两章将涵盖如何构建可靠、可观察的 AI 系统,这些系统能在企业规模上运行。
因此,在开发基于代理的系统时,需要仔细考虑几个关键因素:
-
价值创造:代理必须提供明确的效用,其成本(包括设置、维护和必要的人类监督)低于其价值。这通常意味着从定义明确、价值高的任务开始,自动化可以明显改善结果。
-
信任和安全:随着代理承担更多责任,建立和维护用户信任变得至关重要。这包括技术可靠性和透明的操作,使用户能够理解和预测代理的行为。
-
标准化:随着代理生态系统的增长,标准化的接口和协议对于互操作性变得至关重要。这类似于网络标准的开发,这些标准促进了互联网应用程序的增长。
虽然早期的 AI 系统专注于模式匹配和预定义模板,但现代 AI 代理展示了涌现能力,如推理、问题解决和长期规划。今天的 AI 代理将 LLM 与交互式环境集成,使其能够在复杂领域自主运行。
基于代理的 AI 的发展是从统计模型到深度学习,再到基于推理的系统的一种自然演进。现代 AI 代理利用多模态能力、强化学习和记忆增强架构来适应各种任务。这种演进标志着从预测模型到真正自主的系统,这些系统能够进行动态决策的转变。
展望未来,AI 代理将继续完善其在结构和非结构化环境中的推理、规划和行动能力。开放权重模型的出现,结合基于代理的 AI 的进步,很可能会推动 AI 下一个创新浪潮,扩大其在科学、工程和日常生活中的应用。
使用像 LangChain 这样的框架,开发者可以构建复杂且具有代理能力的结构化系统,克服原始 LLM 的局限性。它提供了内置的内存管理、工具集成和多步推理解决方案,与这里提出的生态系统模型相一致。在下一节中,我们将探讨 LangChain 如何促进生产就绪 AI 代理的开发。
介绍 LangChain
LangChain 作为一个开源框架和风险投资支持的公司存在。该框架由 Harrison Chase 于 2022 年推出,通过支持包括 Python、JavaScript/TypeScript、Go、Rust 和 Ruby 在内的多种编程语言,简化了 LLM 驱动应用程序的开发。
LangChain 框架背后的公司 LangChain, Inc. 位于旧金山,并通过多轮融资获得了显著的风险投资,包括 2024 年 2 月的 A 轮融资。拥有 11-50 名员工,该公司维护和扩展框架,同时提供企业级 LLM 应用程序开发解决方案。
虽然核心框架仍然是开源的,但公司为商业用户提供额外的企业功能和支持。两者拥有相同的使命:通过提供强大的工具和基础设施来加速 LLM 应用开发。
现代 LLMs 无疑是强大的,但它们在生产应用中的实际效用受到几个固有局限性的限制。理解这些挑战对于理解为什么像 LangChain 这样的框架成为 AI 开发者不可或缺的工具至关重要。
原始 LLMs 的挑战
尽管它们的性能令人印象深刻,但大型语言模型(LLMs)面临着一些基本限制,这些限制为开发者构建现实世界应用设置了重大障碍:
-
上下文窗口限制:LLMs 将文本作为令牌(子词单元)处理,而不是完整的单词。例如,“LangChain”可能被处理为两个令牌:“Lang”和“Chain”。每个 LLM 都有一个固定的上下文窗口——它一次可以处理的令牌最大数量——通常在 2,000 到 128,000 个令牌之间。这带来了几个实际挑战:
-
文档处理:长文档必须有效地分块,以适应上下文限制
-
对话历史:在长时间对话中保持信息需要仔细的记忆管理
-
成本管理:大多数提供商根据令牌数量收费,因此高效使用令牌成为一项商业必要条件
-
这些限制直接影响了应用架构,使得像 RAG(我们将在第四章)这样的技术对于生产系统变得至关重要。
-
有限的工具编排:虽然许多现代 LLMs 提供了原生的工具调用功能,但它们缺乏发现适当工具、执行复杂工作流程和管理跨多个回合的工具交互的基础设施。没有这个编排层,开发者必须为每个集成构建定制的解决方案。
-
任务协调挑战:使用 LLMs 管理多步骤工作流程需要结构化的控制机制。没有这些机制,涉及顺序推理或决策的复杂过程难以可靠地实施。
工具在此上下文中指的是扩展 LLM 功能的能力:用于搜索互联网的网页浏览器、用于精确数学的计算机、用于执行程序的编码环境或用于访问外部服务和数据库的 API。没有这些工具,LLMs 将局限于在其训练知识范围内操作,无法执行现实世界的行动或访问当前信息。
这些基本限制为使用原始 LLM API 的开发者带来了三个关键挑战,如下表所示。
挑战 | 描述 | 影响 |
---|---|---|
可靠性 | 检测幻觉并验证输出 | 可能需要人工验证的不一致结果 |
资源管理 | 处理上下文窗口和速率限制 | 实现复杂性和潜在的成本超支 |
集成复杂性 | 建立与外部工具和数据源的联系 | 延长的开发时间和维护负担 |
表 1.3:三个关键的开发者挑战
LangChain 通过提供具有测试解决方案的结构化框架,简化了 AI 应用开发,并使更复杂的使用案例成为可能。
LangChain 如何实现代理开发
LangChain 通过其模块化架构和可组合模式,为构建复杂的 AI 应用提供了基础基础设施。随着版本 0.3 的演进,LangChain 对其创建智能系统的方法进行了优化:
-
可组合工作流程:LangChain 表达式语言(LCEL)允许开发者将复杂任务分解为模块化组件,这些组件可以组装和重新配置。这种可组合性通过多个处理步骤的编排,实现了系统性的推理。
-
集成生态系统:LangChain 为所有生成式 AI 组件(LLMs、嵌入、向量数据库、文档加载器、搜索引擎)提供了经过实战检验的抽象接口。这使得您能够构建可以轻松在提供者之间切换而无需重写核心逻辑的应用程序。
-
统一模型访问:该框架为各种语言和嵌入模型提供了一致的接口,允许在保持应用程序逻辑的同时,在提供者之间无缝切换。
虽然 LangChain 的早期版本直接处理内存管理,但版本 0.3 采用了更专业的方法来开发应用程序:
-
内存和状态管理:对于需要跨交互持久上下文的应用程序,LangGraph 现在作为推荐解决方案。LangGraph 使用专门设计的持久机制维护对话历史和应用程序状态。
-
代理架构:尽管 LangChain 包含代理实现,但 LangGraph 已成为构建复杂代理的首选框架。它提供:
-
基于图的复杂决策路径工作流程定义
-
多次交互中的持久状态管理
-
处理过程中的实时反馈流支持
-
人工验证和校正能力
-
与其配套项目如 LangGraph 和 LangSmith 一起,LangChain 形成了一个全面的生态系统,将 LLM 从简单的文本生成器转变为能够执行复杂现实任务的系统,结合了强大的抽象和针对生产使用优化的实用实现模式。
探索 LangChain 架构
LangChain 的哲学核心在于可组合性和模块化。它不是将 LLM 视为独立的服务,而是将其视为可以与其他工具和服务结合以创建更强大系统的组件。这种方法基于几个原则:
-
模块化架构:每个组件都设计为可重用和可互换的,使开发者能够无缝地将 LLMs 集成到各种应用中。这种模块化不仅限于 LLMs,还包括开发复杂生成式 AI 应用程序的大量构建块。
-
支持代理工作流程:LangChain 提供了业界领先的 API,允许您快速开发复杂的代理。这些代理可以做出决策,使用工具,并以最小的开发开销解决问题。
-
生产就绪:该框架提供了内置的跟踪、评估和部署生成式 AI 应用程序的能力,包括管理交互中内存和持久性的强大构建块。
-
广泛的供应商生态系统:LangChain 为所有生成式 AI 组件(LLMs、嵌入、向量数据库、文档加载器、搜索引擎等)提供了经过实战检验的抽象接口。供应商开发自己的集成,以符合这些接口,允许您在任意第三方提供商之上构建应用程序,并轻松地在它们之间切换。
值得注意的是,自从本书第一版撰写时 LangChain 版本 0.1 以来,已经发生了重大变化。虽然早期版本试图处理所有事情,但 LangChain 版本 0.3 专注于在特定功能上表现出色,而伴随项目则处理专门需求。LangChain 负责模型集成和工作流程管理,LangGraph 负责有状态的代理,LangSmith 提供可观察性。
LangChain 的内存管理也经历了重大变化。基 LangChain 库内的内存机制已被弃用,转而使用 LangGraph 进行持久化,尽管存在代理,但在版本 0.3 中,LangGraph 是创建代理的首选方法。然而,模型和工具仍然是 LangChain 功能的基础。在 第三章 中,我们将探讨 LangChain 和 LangGraph 的内存机制。
为了将模型设计原则转化为实用工具,LangChain 开发了一个全面的库、服务和应用程序生态系统。这个生态系统为开发者提供了构建、部署和维护复杂 AI 应用程序所需的一切。让我们来审视构成这个繁荣环境的组件,以及它们如何在整个行业中得到采用。
生态系统
LangChain 已经实现了令人印象深刻的生态系统指标,显示出强大的市场采用度,月下载量超过 2000 万次,并支持超过 10 万个应用。其开源社区蓬勃发展,由 10 万多个 GitHub 星标和来自 4000 多名开发者的贡献所证明。这种采用规模使 LangChain 成为 AI 应用开发领域的领先框架,尤其是在构建以推理为重点的 LLM 应用方面。该框架的模块化架构(如 LangGraph 用于代理工作流程和 LangSmith 用于监控)显然与各行各业构建生产级 AI 系统的开发者产生了共鸣。
核心库
-
LangChain(Python):构建 LLM 应用的可重用组件
-
LangChain.js:框架的 JavaScript/TypeScript 实现
-
LangGraph(Python):构建 LLM 代理作为编排图的工具
-
LangGraph.js:用于代理工作流程的 JavaScript 实现
平台服务
-
LangSmith:用于调试、测试、评估和监控 LLM 应用的平台
-
LangGraph:部署和扩展 LangGraph 代理的基础设施
应用和扩展
-
ChatLangChain:框架问答文档助手
-
Open Canvas:基于文档和聊天的代码/Markdown 编写 UX(TypeScript)
-
OpenGPTs:OpenAI 的 GPTs API 的开源实现
-
邮件助手:用于电子邮件管理的 AI 工具(Python)
-
社交媒体代理:内容整理和排程代理(TypeScript)
该生态系统为构建以推理为重点的 AI 应用提供了一套完整的解决方案:从核心构建块到部署平台再到参考实现。这种架构允许开发者独立使用组件,或将它们堆叠以获得更全面和完整的解决方案。
来自客户评价和公司合作,LangChain 正在被 Rakuten、Elastic、Ally 和 Adyen 等企业采用。组织报告称,他们使用 LangChain 和 LangSmith 来确定 LLM 实施的优化方法,提高开发人员生产力,并加速开发工作流程。
LangChain 还提供了一套完整的 AI 应用开发栈:
-
构建:使用可组合框架
-
运行:使用 LangGraph 平台部署
-
管理:使用 LangSmith 进行调试、测试和监控
基于我们使用 LangChain 构建的经验,以下是一些我们认为特别有帮助的益处:
-
加速开发周期:LangChain 通过现成的构建块和统一的 API,显著缩短了上市时间,消除了数周集成工作。
-
卓越的可观察性:LangChain 与 LangSmith 的结合为复杂代理行为提供了无与伦比的可见性,使成本、延迟和质量之间的权衡更加透明。
-
受控代理平衡: LangGraph 对代理式 AI 的方法特别强大——允许开发者赋予 LLMs 对工作流程的部分控制流,同时保持可靠性和性能。
-
生产就绪模式: 我们的实施经验证明,LangChain 的架构提供了企业级解决方案,有效减少了幻觉并提高了系统可靠性。
-
未来兼容的灵活性: 框架的供应商无关设计创建的应用程序可以随着 LLM 领域的发展而适应,防止技术锁定。
这些优势直接源于 LangChain 的架构决策,这些决策优先考虑了模块化、可观察性和实际应用的部署灵活性。
模块化设计和依赖管理
LangChain 发展迅速,每天大约合并 10-40 个拉取请求。这种快速的开发节奏,加上框架广泛的集成生态系统,带来了独特的挑战。不同的集成通常需要特定的第三方 Python 包,这可能导致依赖项冲突。
LangChain 的包架构是作为对扩展挑战的直接回应而演化的。随着框架迅速扩展以支持数百个集成,原始的单体结构变得不可持续——迫使用户安装不必要的依赖项,造成维护瓶颈,并阻碍了贡献的可达性。通过划分为具有依赖项懒加载的专用包,LangChain 优雅地解决了这些问题,同时保持了统一的生态系统。这种架构允许开发者仅导入他们需要的部分,减少版本冲突,为稳定与实验性功能提供独立的发布周期,并极大地简化了社区开发者针对特定集成的工作贡献路径。
LangChain 的代码库遵循一个组织良好的结构,在分离关注点的同时保持一个统一的生态系统:
核心结构
-
docs/
: 为开发者提供的文档资源 -
libs/
: 包含 monorepo 中的所有库包
库组织
-
langchain-core/
: 定义框架的基础抽象和接口 -
langchain/
: 包含核心组件的主要实现库: -
vectorstores/
: 与向量数据库(Pinecone、Chroma 等)的集成 -
chains/
: 为常见工作流程预构建的链实现
其他用于检索器、嵌入等组件的组件目录
-
langchain-experimental/
: 正在开发中的前沿特性 -
langchain-community: 由 LangChain 社区维护的第三方集成。这包括大多数针对 LLMs、向量存储和检索器的集成。依赖项是可选的,以保持轻量级的包。
-
合作伙伴包:流行的集成被分离到专门的包中(例如,langchain-openai,langchain-anthropic)以增强独立支持。这些包位于 LangChain 存储库之外,但在 GitHub “langchain-ai” 组织内(见 github.com/orgs/langchain-ai)。完整列表可在 python.langchain.com/v0.3/docs/integrations/platforms/ 上找到。
-
外部合作伙伴包:一些合作伙伴独立维护他们的集成包。例如,来自 Google 组织的几个包(github.com/orgs/googleapis/repositories?q=langchain),如
langchain-google-cloud-sql-mssql
包,是在 LangChain 生态系统之外开发和维护的。
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_01_02.png
图 1.2:集成生态系统图
关于数十个可用模块和包的详细信息,请参阅全面的 LangChain API 参考文档:api.python.langchain.com/
. 此外,还有数百个代码示例展示了实际应用场景:python.langchain.com/v0.1/docs/use_cases/
.
LangGraph、LangSmith 和配套工具
LangChain 的核心功能通过以下配套项目得到扩展:
-
LangGraph:一个用于构建具有状态、多参与者应用的编排框架,使用 LLMs。虽然它与 LangChain 集成顺畅,但也可以独立使用。LangGraph 促进了具有循环数据流的复杂应用程序,并支持流式传输和人工交互。我们将在第三章中更详细地讨论 LangGraph。
-
LangSmith:一个通过提供强大的调试、测试和监控功能来补充 LangChain 的平台。开发者可以检查、监控和评估他们的应用程序,确保持续优化和自信部署。
这些扩展以及核心框架提供了一套全面的生态系统,用于开发、管理和可视化 LLM 应用程序,每个都具有独特的功能,增强了功能和用户体验。
LangChain 还拥有广泛的工具集成,我们将在第五章中详细讨论。新集成定期添加,扩展了框架在各个领域的功能。
第三方应用程序和可视化工具
许多第三方应用都是基于 LangChain 构建的。例如,LangFlow 和 Flowise 引入了 LLM 开发的可视化界面,具有允许将 LangChain 组件拖放到可执行工作流程中的 UI。这种可视化方法使得快速原型设计和实验成为可能,降低了创建复杂管道的门槛,如下面的 Flowise 截图所示:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_01_03.png
图 1.3:使用 LLM、计算器和搜索工具的代理的 Flowise UI(来源:https://github.com/FlowiseAI/Flowise)
在上面的 UI 中,你可以看到一个连接到搜索界面(Serp API)、LLM 和计算器的代理。LangChain 和类似工具可以使用 Chainlit 等库在本地部署,或者在包括 Google Cloud 在内的各种云平台上部署。
总结来说,LangChain 通过其模块化设计、广泛的集成和支持性生态系统简化了 LLM 应用的开发。这使得它成为开发者构建复杂人工智能系统而不必重新发明基本组件的无价之宝。
摘要
本章介绍了现代 LLM 的格局,并将 LangChain 定位为构建生产就绪人工智能应用的有力框架。我们探讨了原始 LLM 的限制,然后展示了这些框架如何将模型转化为可靠、智能的系统,能够解决复杂现实世界问题。我们还考察了 LangChain 生态系统架构,包括其模块化组件、包结构和支持完整开发生命周期的配套项目。通过理解 LLM 及其扩展框架之间的关系,你现在可以构建超越简单文本生成的应用。
在下一章中,我们将设置我们的开发环境,并使用 LangChain 迈出第一步,将本章的概念理解转化为实际代码。你将学习如何连接到各种 LLM 提供商,创建你的第一个链,并开始实现构成企业级人工智能应用基础的模式。
问题
-
原始 LLM 的三个主要限制是什么,它们如何影响生产应用,以及 LangChain 如何解决每一个问题?
-
从部署选项、成本考虑和使用案例等方面比较开源和闭源 LLM。你可能在什么情况下选择每种类型?
-
LangChain 链和 LangGraph 代理之间的区别是什么?在什么情况下你会选择其中一个而不是另一个?
-
解释 LangChain 模块化架构如何支持人工智能应用的快速开发。提供一个例子说明这种模块化如何使企业用例受益。
-
LangChain 生态系统的关键组件是什么,它们是如何协同工作以支持从构建到部署再到监控的开发生命周期的?
-
代理式 AI 与传统 LLM 应用有何不同?描述一个代理相对于简单链能提供显著优势的商业场景。
-
在为生产应用程序选择 LLM 提供商时,应考虑哪些因素?请列出至少三个除了模型性能之外的考虑因素。
-
LangChain 如何帮助解决所有 LLM 应用程序都面临的常见挑战,如幻觉、上下文限制和工具集成?
-
解释 LangChain 包结构(
langchain-core
,langchain
,langchain-community
)如何影响应用程序中的依赖管理和集成选项。 -
LangSmith 在生产 LangChain 应用程序的生命周期中扮演什么角色?
第三章:LangChain 的第一步
在上一章中,我们探讨了大型语言模型(LLMs)并介绍了 LangChain 作为构建 LLM 驱动的应用程序的强大框架。我们讨论了 LLMs 如何通过理解上下文、生成类似人类的文本和执行复杂推理的能力而彻底改变了自然语言处理。虽然这些功能令人印象深刻,但我们还考察了它们的局限性——幻觉、上下文限制和缺乏最新知识。
在本章中,我们将通过构建我们的第一个 LangChain 应用程序,从理论转向实践。我们将从基础开始:设置合适的发展环境,理解 LangChain 的核心组件,并创建简单的链。从那里,我们将探索更高级的功能,包括为了隐私和成本效益运行本地模型,以及构建结合文本和视觉理解的跨模态应用程序。到本章结束时,你将拥有 LangChain 构建块的良好基础,并准备好在后续章节中创建越来越复杂的 AI 应用程序。
总结来说,本章将涵盖以下主题:
-
设置依赖项
-
探索 LangChain 的构建块(模型接口、提示和模板以及 LCEL)
-
运行本地模型
-
跨模态 AI 应用程序
由于 LangChain 和更广泛的 AI 领域都在快速发展,我们在 GitHub 仓库中维护了最新的代码示例和资源:github.com/benman1/generative_ai_with_langchain
。
如有疑问或需要故障排除帮助,请在 GitHub 上创建一个问题或加入我们的 Discord 社区:packt.link/lang
。
为本书设置依赖项
本书提供了多种运行代码示例的选项,从零配置的云笔记本到本地开发环境。选择最适合你经验和偏好的方法。即使你熟悉依赖项管理,也请阅读这些说明,因为本书中的所有代码都将依赖于此处概述的正确环境安装。
如果不需要本地设置,我们为每一章提供现成的在线笔记本:
-
Google Colab:使用免费 GPU 访问运行示例
-
Kaggle 笔记本:在集成数据集上进行实验
-
梯度笔记本:访问高性能计算选项
你在这本书中找到的所有代码示例都可以在 GitHub 上以在线笔记本的形式找到,网址为 github.com/benman1/generative_ai_with_langchain
。
这些笔记本没有预先配置所有依赖项,但通常只需要几个安装命令就可以开始。这些工具允许你立即开始实验,无需担心设置。如果你更喜欢在本地工作,我们建议使用 conda 进行环境管理:
-
如果你还没有安装 Miniconda,请先安装。
-
使用 Python 3.11 创建一个新的环境:
conda create -n langchain-book python=3.11
-
激活环境:
conda activate langchain-book
-
安装 Jupyter 和核心依赖项:
conda install jupyter pip install langchain langchain-openai jupyter
-
启动 Jupyter Notebook:
jupyter notebook
这种方法为使用 LangChain 提供了一个干净、隔离的工作环境。对于有固定工作流程的资深开发者,我们还支持:
-
pip with venv:GitHub 仓库中的说明
-
Docker 容器:GitHub 仓库中提供的 Dockerfile
-
Poetry:GitHub 仓库中可用的配置文件
选择你最舒适的方法,但请记住,所有示例都假设有一个 Python 3.10+环境,并具有 requirements.txt 中列出的依赖项。
对于开发者来说,Docker,通过容器提供隔离,是一个不错的选择。缺点是它占用大量磁盘空间,并且比其他选项更复杂。对于数据科学家,我推荐使用 Conda 或 Poetry。
Conda 在处理复杂依赖项方面效率很高,尽管在大环境中可能会非常慢。Poetry 很好地解决依赖项并管理环境;然而,它不捕获系统依赖项。
所有工具都允许从配置文件中共享和复制依赖项。你可以在书的 GitHub 仓库github.com/benman1/generative_ai_with_langchain
中找到一组说明和相应的配置文件。
完成后,请确保你已经安装了 LangChain 版本 0.3.17。你可以使用命令pip show langchain
来检查。
随着 LLM 领域的创新步伐加快,库的更新很频繁。本书中的代码是用 LangChain 0.3.17 测试的,但新版本可能会引入变化。如果你在运行示例时遇到任何问题:
-
在我们的 GitHub 仓库创建一个问题
-
在
packt.link/lang
上的 Discord 加入讨论 -
在书的 Packt 页面上检查勘误表
这种社区支持确保你能够成功实施所有项目,无论库的更新如何。
API 密钥设置
LangChain 的无提供商方法支持广泛的 LLM 提供商,每个都有其独特的优势和特点。除非你使用本地 LLM,否则要使用这些服务,你需要获得适当的认证凭据。
提供商 | 环境变量 | 设置 URL | 免费层 |
---|---|---|---|
OpenAI | OPENAI_API_KEY |
platform.openai.com | 否 |
HuggingFace | HUGGINGFACEHUB_API_TOKEN |
huggingface.co/settings/tokens | 是 |
Anthropic | ANTHROPIC_API_KEY |
console.anthropic.com | 否 |
Google AI | GOOGLE_API_KEY |
ai.google.dev/gemini-api | 是 |
Google VertexAI | 应用程序默认凭证 |
cloud.google.com/vertex-ai | 是(有限制) |
Replicate | REPLICATE_API_TOKEN |
replicate.com | 否 |
表 2.1:API 密钥参考表(概述)
大多数提供商需要 API 密钥,而像 AWS 和 Google Cloud 这样的云提供商也支持其他身份验证方法,如 应用程序默认凭证(ADC)。许多提供商提供免费层,无需信用卡详细信息,这使得入门变得容易。
要在环境中设置 API 密钥,在 Python 中,我们可以执行以下行:
import os
os.environ["OPENAI_API_KEY"] = "<your token>"
在这里,OPENAI_API_KEY
是适用于 OpenAI 的环境密钥。在您的环境中设置密钥的优点是,每次使用模型或服务集成时,无需将它们作为参数包含在您的代码中。
您也可以从终端在您的系统环境中暴露这些变量。在 Linux 和 macOS 中,您可以使用 export
命令从终端设置系统环境变量:
export OPENAI_API_KEY=<your token>
要在 Linux 或 macOS 中永久设置环境变量,您需要将前面的行添加到 ~/.bashrc
或 ~/.bash_profile
文件中,然后使用命令 source ~/.bashrc
或 source ~/.bash_profile
重新加载 shell。
对于 Windows 用户,您可以通过在系统设置中搜索“环境变量”来设置环境变量,编辑“用户变量”或“系统变量”,并添加 export
OPENAI_API_KEY=your_key_here
。
我们的选择是创建一个 config.py
文件,其中存储所有 API 密钥。然后我们从该模块导入一个函数,将这些密钥加载到环境变量中。这种方法集中管理凭证,并在需要时更容易更新密钥:
import os
OPENAI_API_KEY = "... "
# I'm omitting all other keys
def set_environment():
variable_dict = globals().items()
for key, value in variable_dict:
if "API" in key or "ID" in key:
os.environ[key] = value
如果您在 GitHub 仓库中搜索此文件,您会注意到它缺失。这是故意的 - 我已经使用 .gitignore
文件将其排除在 Git 跟踪之外。.gitignore
文件告诉 Git 在提交更改时要忽略哪些文件,这对于:
-
防止敏感凭证被公开暴露
-
避免意外提交个人 API 密钥
-
保护自己免受未经授权的使用费用
要自行实现此功能,只需将 config.py
添加到您的 .gitignore
文件中:
# In .gitignore
config.py
.env
**/api_keys.txt
# Other sensitive files
您可以在 config.py
文件中设置所有您的密钥。此函数 set_environment()
将所有密钥加载到环境变量中,如前所述。任何您想要运行应用程序的时候,您都可以导入此函数并像这样运行它:
from config import set_environment
set_environment()
对于生产环境,考虑使用专用的密钥管理服务或运行时注入的环境变量。这些方法提供了额外的安全性,同时保持了代码和凭证之间的分离。
虽然 OpenAI 的模型仍然具有影响力,但 LLM 生态系统已经迅速多元化,为开发者提供了多种应用选项。为了保持清晰,我们将 LLM 与其提供访问权限的模型网关分开。
-
关键 LLM 家族
-
Anthropic Claude:在推理、长文本内容处理和视觉分析方面表现出色,具有高达 200K 个 token 的上下文窗口
-
Mistral 模型:功能强大的开源模型,具有强大的多语言能力和卓越的推理能力
-
Google Gemini:具有行业领先的 1M 个 token 上下文窗口和实时信息访问的高级多模态模型
-
OpenAI GPT-o:具有领先的跨模态能力,接受文本、音频、图像和视频,并具有增强的推理能力
-
DeepSeek 模型:专注于编码和技术推理,在编程任务上具有最先进的性能
-
AI21 Labs Jurassic:在学术应用和长文本内容生成方面表现强劲
-
Inflection Pi:针对对话 AI 优化,具有卓越的情感智能
-
Perplexity 模型:专注于为研究应用提供准确、有引用的答案
-
Cohere 模型:针对企业应用,具有强大的多语言能力
-
-
云提供商网关
-
Amazon Bedrock:通过 AWS 集成提供 Anthropic、AI21、Cohere、Mistral 和其他模型的一站式 API 访问
-
Azure OpenAI 服务:提供企业级访问 OpenAI 和其他模型,具有强大的安全性和微软生态系统集成
-
Google Vertex AI:通过无缝的 Google Cloud 集成访问 Gemini 和其他模型
-
-
独立平台
-
Together AI:托管 200 多个开源模型,提供无服务器和专用 GPU 选项
-
Replicate:专注于部署按使用付费的多模态开源模型
-
HuggingFace 推理端点:具有微调能力的数千个开源模型的量产部署
-
在本书中,我们将与通过不同提供商访问的各种模型一起工作,为您提供选择最适合您特定需求和基础设施要求的最佳选项的灵活性。
我们将使用 OpenAI 进行许多应用,但也会尝试来自其他组织的 LLM。请参考本书末尾的附录了解如何获取 OpenAI、Hugging Face、Google 和其他提供商的 API 密钥。
有两个主要的集成包:
-
langchain-google-vertexai
-
langchain-google-genai
我们将使用 LangChain 推荐的langchain-google-genai
包,对于个人开发者来说,设置要简单得多,只需一个 Google 账户和 API 密钥。对于更大的项目,建议迁移到langchain-google-vertexai
。此集成提供了企业功能,如客户加密密钥、虚拟私有云集成等,需要具有计费功能的 Google Cloud 账户。
如果你已经按照上一节中指示的 GitHub 上的说明操作,那么你应该已经安装了langchain-google-genai
包。
探索 LangChain 的构建块
为了构建实际的应用程序,我们需要了解如何与不同的模型提供者合作。让我们探索从云服务到本地部署的各种选项。我们将从 LLM 和聊天模型等基本概念开始,然后深入到提示、链和记忆系统。
模型接口
LangChain 提供了一个统一的接口来处理各种 LLM 提供者。这种抽象使得在保持一致代码结构的同时轻松地在不同模型之间切换变得容易。以下示例演示了如何在实际场景中实现 LangChain 的核心组件。
请注意,用户几乎应该只使用较新的聊天模型,因为大多数模型提供者已经采用了类似聊天的界面来与语言模型交互。我们仍然提供 LLM 接口,因为它作为字符串输入、字符串输出非常容易使用。
LLM 交互模式
LLM 接口代表传统的文本完成模型,它接受字符串输入并返回字符串输出。在 LangChain 中越来越多的用例仅使用 ChatModel 接口,主要是因为它更适合构建复杂的工作流程和开发代理。LangChain 文档现在正在弃用 LLM 接口,并推荐使用基于聊天的接口。虽然本章演示了这两个接口,但我们建议使用聊天模型,因为它们代表了 LangChain 的当前标准,以便保持最新。
让我们看看 LLM 接口的实际应用:
from langchain_openai import OpenAI
from langchain_google_genai import GoogleGenerativeAI
# Initialize OpenAI model
openai_llm = OpenAI()
# Initialize a Gemini model
gemini_pro = GoogleGenerativeAI(model="gemini-1.5-pro")
# Either one or both can be used with the same interface
response = openai_llm.invoke("Tell me a joke about light bulbs!")
print(response)
请注意,当你运行此程序时,你必须设置你的环境变量为提供者的密钥。例如,当运行此程序时,我会首先通过调用set_environment()
从config
文件开始:
from config import set_environment
set_environment()
我们得到以下输出:
Why did the light bulb go to therapy?
Because it was feeling a little dim!
对于 Gemini 模型,我们可以运行:
response = gemini_pro.invoke("Tell me a joke about light bulbs!")
对于我来说,Gemini 提出了这个笑话:
Why did the light bulb get a speeding ticket?
Because it was caught going over the watt limit!
注意我们如何无论提供者如何都使用相同的invoke()
方法。这种一致性使得在实验不同模型或在生产中切换提供者变得容易。
开发测试
在开发过程中,你可能想在不实际进行 API 调用的情况下测试你的应用程序。LangChain 提供了FakeListLLM
用于此目的:
from langchain_community.llms import FakeListLLM
# Create a fake LLM that always returns the same response
fake_llm = FakeListLLM(responses=["Hello"])
result = fake_llm.invoke("Any input will return Hello")
print(result) # Output: Hello
与聊天模型合作
聊天模型是针对模型与人类之间多轮交互进行微调的 LLM。如今,大多数 LLM 都是针对多轮对话进行微调的。而不是向模型提供输入,例如:
human: turn1
ai: answer1
human: turn2
ai: answer2
在我们期望它通过继续对话生成输出时,这些天模型提供者通常提供一个 API,该 API 期望每个回合作为有效载荷中格式良好的独立部分。模型提供者通常不会在服务器端存储聊天历史,他们每次都从客户端接收完整的历史记录,并在服务器端仅格式化最终提示。
LangChain 与 ChatModels 采用了相同的模式,通过具有角色和内容的结构化消息处理对话。每条消息包含:
-
角色(谁在说话),由消息类(所有消息都继承自 BaseMessage)定义
-
内容(所说的内容)
消息类型包括:
-
SystemMessage
:设置模型的行为和上下文。例如:SystemMessage(content="You're a helpful programming assistant")
-
HumanMessage
:表示用户输入,如问题、命令和数据。例如:HumanMessage(content="Write a Python function to calculate factorial")
-
AIMessage
:包含模型响应
让我们看看这个动作:
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage, HumanMessage
chat = ChatAnthropic(model="claude-3-opus-20240229")
messages = [
SystemMessage(content="You're a helpful programming assistant"),
HumanMessage(content="Write a Python function to calculate factorial")
]
response = chat.invoke(messages)
print(response)
克劳德提出了一个函数、解释和调用函数的示例。
这里是一个计算给定数字阶乘的 Python 函数:
```python
def factorial(n):
if n < 0:
raise ValueError("负数没有定义阶乘。")
elif n == 0:
return 1
else:
result = 1
for i in range(1, n + 1):
result *= i
return result
```py
Let's break that down. The factorial function is designed to take an integer n as input and calculate its factorial. It starts by checking if n is negative, and if so, it raises a ValueError since factorials aren't defined for negative numbers. If n is zero, the function returns 1, which makes sense because, by definition, the factorial of 0 is 1.
When dealing with positive numbers, the function kicks things off by setting a variable result to 1\. From there, it enters a loop that runs from 1 to n, inclusive, thanks to the range function. During each step of the loop, it multiplies the result by the current number, gradually building up the factorial. Once the loop completes, the function returns the final calculated value. You can call this function by providing a non-negative integer as an argument. Here are a few examples:
```python
print(factorial(0)) # 输出:1
print(factorial(5)) # 输出:120
print(factorial(10)) # 输出:3628800
print(factorial(-5)) # 抛出 ValueError:负数没有定义阶乘。
```py
Note that the factorial function grows very quickly, so calculating the factorial of large numbers may exceed the maximum representable value in Python. In such cases, you might need to use a different approach or a library that supports arbitrary-precision arithmetic.
同样,我们也可以询问 OpenAI 的模型,如 GPT-4 或 GPT-4o:
from langchain_openai.chat_models import ChatOpenAI
chat = ChatOpenAI(model_name='gpt-4o')
推理模型
Anthropic 的 Claude 3.7 Sonnet 引入了一种名为 扩展思考 的强大功能,允许模型在提供最终答案之前展示其推理过程。这一功能代表了开发者如何利用 LLMs 进行复杂推理任务的重大进步。
这是如何通过 ChatAnthropic 类配置扩展思考的:
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
# Create a template
template = ChatPromptTemplate.from_messages([
("system", "You are an experienced programmer and mathematical analyst."),
("user", "{problem}")
])
# Initialize Claude with extended thinking enabled
chat = ChatAnthropic(
model_name="claude-3-7-sonnet-20240326", # Use latest model version
max_tokens=64_000, # Total response length limit
thinking={"type": "enabled", "budget_tokens": 15000}, # Allocate tokens for thinking
)
# Create and run a chain
chain = template | chat
# Complex algorithmic problem
problem = """
Design an algorithm to find the kth largest element in an unsorted array
with the optimal time complexity. Analyze the time and space complexity
of your solution and explain why it's optimal.
"""
# Get response with thinking included
response = chat.invoke([HumanMessage(content=problem)])
print(response.content)
响应将包括克劳德关于算法选择、复杂度分析和优化考虑的逐步推理,在呈现最终解决方案之前。在先前的例子中:
-
在 64,000 个令牌的最大响应长度中,最多可以使用 15,000 个令牌用于克劳德的思考过程。
-
剩余的 ~49,000 个令牌可用于最终响应。
-
克劳德并不总是使用全部的思考预算——它只使用特定任务所需的预算。如果克劳德用完了思考令牌,它将过渡到最终答案。
虽然 克劳德 提供了显式的思考配置,但你也可以通过不同的技术通过其他提供商获得类似(但不完全相同)的结果:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
template = ChatPromptTemplate.from_messages([
("system", "You are a problem-solving assistant."),
("user", "{problem}")
])
# Initialize with reasoning_effort parameter
chat = ChatOpenAI(
model="o3-mini","
reasoning_effort="high" # Options: "low", "medium", "high"
)
chain = template | chat
response = chain.invoke({"problem": "Calculate the optimal strategy for..."})
chat = ChatOpenAI(model="gpt-4o")
chain = template | chat
response = chain.invoke({"problem": "Calculate the optimal strategy for..."})
reasoning_effort
参数通过消除对复杂推理提示的需求,允许你在速度比详细分析更重要时通过减少努力来调整性能,并有助于通过控制推理过程所需的处理能力来管理令牌消耗。
DeepSeek 模型还通过 LangChain 集成提供显式的思考配置。
控制模型行为
理解如何控制大型语言模型(LLM)的行为对于调整其输出以满足特定需求至关重要。如果没有仔细调整参数,模型可能会产生过于创意、不一致或冗长的响应,这些响应不适合实际应用。例如,在客户服务中,你希望得到一致、事实性的答案,而在内容生成中,你可能希望得到更多创意和促销的输出。
LLMs 提供了一些参数,允许对生成行为进行精细控制,尽管具体的实现可能因提供商而异。让我们探讨其中最重要的几个:
参数 | 描述 | 典型范围 | 最佳用途 |
---|---|---|---|
温度 | 控制文本生成的随机性 | 0.0-1.0(OpenAI,Anthropic)0.0-2.0(Gemini) | 较低(0.0-0.3):事实性任务,问答 |
Top-k | 限制标记选择为 k 个最可能的标记 | 1-100 | 较低值(1-10):更聚焦的输出 |
Top-p(核采样) | 考虑标记直到累积概率达到阈值 | 0.0-1.0 | 较低值(0.5):更聚焦的输出 |
|
最大标记数
限制最大响应长度 | 模型特定 | 控制成本和防止冗长输出 |
---|---|---|
存在/频率惩罚 | 通过惩罚已出现的标记来阻止重复 | -2.0 到 2.0 |
停止序列 | 告诉模型何时停止生成 | 自定义字符串 |
表 2.2:LLM 提供的参数
这些参数共同塑造模型输出:
-
温度 + Top-k/Top-p:首先,Top-k/Top-p 过滤标记分布,然后温度影响过滤集内的随机性。
-
惩罚 + 温度:较高的温度和较低的惩罚可以产生创意但可能重复的文本。
LangChain 为在不同 LLM 提供商之间设置这些参数提供了一个一致的接口:
from langchain_openai import OpenAI
# For factual, consistent responses
factual_llm = OpenAI(temperature=0.1, max_tokens=256)
# For creative brainstorming
creative_llm = OpenAI(temperature=0.8, top_p=0.95, max_tokens=512)
一些建议的特定提供商考虑因素:
-
OpenAI:以在 0.0-1.0 范围内温度的一致行为而闻名
-
Anthropic:可能需要较低的温度设置才能达到与其他提供商相似的创意水平。
-
Gemini:支持高达 2.0 的温度,允许在较高设置下实现更极端的创意
-
开源模型:通常需要与商业 API 不同的参数组合。
为应用选择参数
对于需要一致性和准确性的企业应用,通常更倾向于使用较低的温度(0.0-0.3)和适中的 top-p 值(0.5-0.7)。对于创意助手或头脑风暴工具,较高的温度会产生更多样化的输出,尤其是在搭配较高的 top-p 值时。
记住参数调整通常是经验性的——从提供商的建议开始,然后根据您的具体应用程序需求和观察到的输出进行调整。
提示和模板
提示工程是 LLM 应用程序开发的关键技能,尤其是在生产环境中。LangChain 提供了一个强大的系统来管理提示,其功能解决了常见的开发挑战:
-
模板系统以动态生成提示
-
提示管理和版本控制以跟踪更改
-
少量示例管理以提高模型性能
-
输出解析和验证以获得可靠的结果
LangChain 的提示模板将静态文本转换为具有变量替换的动态提示——比较这两种方法以查看关键差异:
-
静态使用——在规模上存在问题:
def generate_prompt(question, context=None): if context: return f"Context information: {context}\n\nAnswer this question concisely: {question}" return f"Answer this question concisely: {question}" # example use: prompt_text = generate_prompt("What is the capital of France?")
-
PromptTemplate – 生产就绪:
from langchain_core.prompts import PromptTemplate # Define once, reuse everywhere question_template = PromptTemplate.from_template( "Answer this question concisely: {question}" ) question_with_context_template = PromptTemplate.from_template( "Context information: {context}\n\nAnswer this question concisely: {question}" ) # Generate prompts by filling in variables prompt_text = question_template.format(question="What is the capital of France?")
模板很重要——原因如下:
-
一致性:它们在您的应用程序中标准化提示。
-
可维护性:它们允许您在一个地方更改提示结构,而不是在整个代码库中。
-
可读性:它们清楚地分离了模板逻辑和业务逻辑。
-
可测试性:单独对提示生成进行单元测试比从 LLM 调用中分离出来更容易。
在生产应用程序中,您通常会需要管理数十或数百个提示。模板提供了一种可扩展的方式来组织这种复杂性。
聊天提示模板
对于聊天模型,我们可以创建更多结构化的提示,这些提示融合了不同的角色:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
template = ChatPromptTemplate.from_messages([
("system", "You are an English to French translator."),
("user", "Translate this to French: {text}")
])
chat = ChatOpenAI()
formatted_messages = template.format_messages(text="Hello, how are you?")
response = chat.invoke(formatted_messages)
print(response)
让我们从LangChain 表达式语言(LCEL)开始,它提供了一种干净、直观的方式来构建 LLM 应用程序。
LangChain 表达式语言 (LCEL)
LCEL 代表了我们构建使用 LangChain 的 LLM 应用程序方式的重大进步。于 2023 年 8 月推出,LCEL 是构建复杂 LLM 工作流的一种声明式方法。LCEL 不关注如何执行每个步骤,而是让您定义想要完成什么,从而允许 LangChain 在幕后处理执行细节。
在其核心,LCEL 作为一个极简代码层,使得连接不同的 LangChain 组件变得非常容易。如果您熟悉 Unix 管道或 pandas 等数据处理库,您会认识到直观的语法:组件通过管道运算符(|)连接以创建处理管道。
如我们在第一章中简要介绍的,LangChain 一直使用“链”的概念作为其连接组件的基本模式。链代表将输入转换为输出的操作序列。
最初,LangChain 通过特定的 Chain
类如 LLMChain
和 ConversationChain
实现了此模式。虽然这些遗留类仍然存在,但它们已被弃用,转而采用更灵活、更强大的 LCEL 方法,该方法建立在可运行接口之上。
Runnable 接口是现代 LangChain 的基石。Runnable 是指任何可以以标准化的方式处理输入并产生输出的组件。每个使用 LCEL 构建的组件都遵循此接口,它提供了一致的方法,包括:
-
invoke()
: 同步处理单个输入并返回输出 -
stream()
: 以生成时的形式输出流 -
batch()
: 高效并行处理多个输入 -
ainvoke()
、abatch()
、astream()
:上述方法的异步版本
这种标准化意味着任何 Runnable 组件——无论是 LLM、提示模板、文档检索器还是自定义函数——都可以连接到任何其他 Runnable,从而创建一个强大的可组合性系统。
每个 Runnable 实现了一组一致的方法,包括:
-
invoke()
: 同步处理单个输入并返回输出 -
stream()
: 以生成时的形式输出流
这种标准化非常强大,因为它意味着任何 Runnable 组件——无论是 LLM、提示模板、文档检索器还是自定义函数——都可以连接到任何其他 Runnable。此接口的一致性使得可以从更简单的构建块构建复杂的应用程序。
LCEL 提供了几个优势,使其成为构建 LangChain 应用程序的首选方法:
-
快速开发:声明性语法使得快速原型设计和复杂链的迭代变得更快。
-
生产就绪功能:LCEL 提供了对流、异步执行和并行处理的内置支持。
-
可读性提高:管道语法使得可视化数据流通过你的应用程序变得容易。
-
无缝生态系统集成:使用 LCEL 构建的应用程序可以自动与 LangSmith 进行监控和 LangServe 进行部署。
-
可定制性:使用 RunnableLambda 轻松将自定义 Python 函数集成到你的链中。
-
运行时优化:LangChain 可以自动优化 LCEL 定义的链的执行。
当需要构建复杂的应用程序,这些应用程序结合了多个组件在复杂的流程中时,LCEL 真正大放异彩。在接下来的章节中,我们将探讨如何使用 LCEL 构建实际的应用程序,从基本的构建块开始,并逐步引入更高级的模式。
管道操作符(|)是 LCEL 的基石,允许你按顺序连接组件:
# 1\. Basic sequential chain: Just prompt to LLM
basic_chain = prompt | llm | StrOutputParser()
在这里,StrOutputParser()
是一个简单的输出解析器,它从 LLM 中提取字符串响应。它将 LLM 的结构化输出转换为普通字符串,使其更容易处理。这个解析器在只需要文本内容而不需要元数据时特别有用。
在底层,LCEL 使用 Python 的操作符重载将这个表达式转换成一个 RunnableSequence,其中每个组件的输出流向下一个组件的输入。管道(|)是语法糖,它覆盖了__or__
隐藏方法,换句话说,A | B
等价于B.__or__(A)
。
管道语法等价于程序性地创建一个RunnableSequence
:
chain = RunnableSequence(first= prompt, middle=[llm], last= output_parser)
LCEL also supports adding transformations and custom functions:
with_transformation = prompt | llm | (lambda x: x.upper()) | StrOutputParser()
对于更复杂的工作流程,你可以结合分支逻辑:
decision_chain = prompt | llm | (lambda x: route_based_on_content(x)) | {
"summarize": summarize_chain,
"analyze": analyze_chain
}
非 Runnable 元素,如函数和字典,会自动转换为适当的 Runnable 类型:
# Function to Runnable
length_func = lambda x: len(x)
chain = prompt | length_func | output_parser
# Is converted to:
chain = prompt | RunnableLambda(length_func) | output_parser
LCEL 的灵活和可组合特性将使我们能够用优雅、可维护的代码解决实际的 LLM 应用挑战。
使用 LCEL 的简单工作流程
正如我们所见,LCEL 提供了一个声明性语法,用于使用管道操作符组合 LLM 应用程序组件。与传统的命令式代码相比,这种方法大大简化了工作流程构建。让我们构建一个简单的笑话生成器来查看 LCEL 的实际应用:
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
# Create components
prompt = PromptTemplate.from_template("Tell me a joke about {topic}")
llm = ChatOpenAI()
output_parser = StrOutputParser()
# Chain them together using LCEL
chain = prompt | llm | output_parser
# Execute the workflow with a single call
result = chain.invoke({"topic": "programming"})
print(result)
这产生了一个编程笑话:
Why don't programmers like nature?
It has too many bugs!
没有 LCEL,相同的流程等同于单独的函数调用,并手动传递数据:
formatted_prompt = prompt.invoke({"topic": "programming"})
llm_output = llm.invoke(formatted_prompt)
result = output_parser.invoke(llm_output)
如你所见,我们已经将链式构建与其执行分离。
在生产应用中,当处理具有分支逻辑、错误处理或并行处理的复杂工作流程时,这种模式变得更加有价值——这些内容我们将在第三章中探讨。
复杂链示例
虽然简单的笑话生成器展示了基本的 LCEL 使用,但现实世界的应用通常需要更复杂的数据处理。让我们通过一个故事生成和分析示例来探索高级模式。
在这个例子中,我们将构建一个多阶段工作流程,展示如何:
-
使用一次 LLM 调用生成内容
-
将该内容输入到第二次 LLM 调用
-
在整个链中保留和转换数据
from langchain_core.prompts import PromptTemplate
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.output_parsers import StrOutputParser
# Initialize the model
llm = GoogleGenerativeAI(model="gemini-1.5-pro")
# First chain generates a story
story_prompt = PromptTemplate.from_template("Write a short story about {topic}")
story_chain = story_prompt | llm | StrOutputParser()
# Second chain analyzes the story
analysis_prompt = PromptTemplate.from_template(
"Analyze the following story's mood:\n{story}"
)
analysis_chain = analysis_prompt | llm | StrOutputParser()
我们可以将这两个链组合在一起。我们的第一个简单方法直接将故事管道输入到分析链中:
# Combine chains
story_with_analysis = story_chain | analysis_chain
# Run the combined chain
story_analysis = story_with_analysis.invoke({"topic": "a rainy day"})
print("\nAnalysis:", story_analysis)
我得到了一个长的分析。这是它的开始:
Analysis: The mood of the story is predominantly **calm, peaceful, and subtly romantic.** There's a sense of gentle melancholy brought on by the rain and the quiet emptiness of the bookshop, but this is balanced by a feeling of warmth and hope.
虽然这可行,但我们已经失去了结果中的原始故事——我们只得到了分析!在生产应用中,我们通常希望在整个链中保留上下文:
from langchain_core.runnables import RunnablePassthrough
# Using RunnablePassthrough.assign to preserve data
enhanced_chain = RunnablePassthrough.assign(
story=story_chain # Add 'story' key with generated content
).assign(
analysis=analysis_chain # Add 'analysis' key with analysis of the story
)
# Execute the chain
result = enhanced_chain.invoke({"topic": "a rainy day"})
print(result.keys()) # Output: dict_keys(['topic', 'story', 'analysis']) # dict_keys(['topic', 'story', 'analysis'])
为了对输出结构有更多控制,我们也可以手动构建字典:
from operator import itemgetter
# Alternative approach using dictionary construction
manual_chain = (
RunnablePassthrough() | # Pass through input
{
"story": story_chain, # Add story result
"topic": itemgetter("topic") # Preserve original topic
} |
RunnablePassthrough().assign( # Add analysis based on story
analysis=analysis_chain
)
)
result = manual_chain.invoke({"topic": "a rainy day"})
print(result.keys()) # Output: dict_keys(['story', 'topic', 'analysis'])
我们可以使用 LCEL 缩写进行字典转换来简化这个过程:
# Simplified dictionary construction
simple_dict_chain = story_chain | {"analysis": analysis_chain}
result = simple_dict_chain.invoke({"topic": "a rainy day"}) print(result.keys()) # Output: dict_keys(['analysis', 'output'])
这些例子比我们的简单笑话生成器更复杂的是什么?
-
数据转换:使用
RunnablePassthrough
和itemgetter
等工具来管理和转换数据 -
字典保留:在整个链中维护上下文,而不仅仅是传递单个值
-
结构化输出:创建结构化输出字典而不是简单的字符串
这些模式对于需要在生产应用中进行以下操作的情况至关重要:
-
跟踪生成内容的来源
-
结合多个操作的结果
-
结构化数据以进行下游处理或显示
-
实现更复杂的错误处理
虽然 LCEL 以优雅的方式处理许多复杂的工作流程,但对于状态管理和高级分支逻辑,您可能希望探索 LangGraph,我们将在 第三章 中介绍。
虽然我们之前的示例使用了基于云的模型,如 OpenAI 和 Google 的 Gemini,但 LangChain 的 LCEL 和其他功能也与本地模型无缝协作。这种灵活性允许您根据特定需求选择正确的部署方法。
运行本地模型
当使用 LangChain 构建 LLM 应用时,您需要决定模型将运行在哪里。
-
本地模型的优点:
-
完全的数据控制和隐私
-
无 API 成本或使用限制
-
无网络依赖
-
控制模型参数和微调
-
-
云模型的优点:
-
无硬件要求或设置复杂性
-
访问最强大、最前沿的模型
-
无需基础设施管理即可弹性扩展
-
无需手动更新即可持续改进模型
-
-
选择本地模型的时候:
-
对数据隐私要求严格的应用
-
开发和测试环境
-
边缘或离线部署场景
-
对成本敏感的应用,具有可预测的高容量使用
-
让我们从最符合开发者友好的本地模型运行选项之一开始。
开始使用 Ollama
Ollama 提供了一种开发者友好的方式来本地运行强大的开源模型。它提供了一个简单的界面来下载和运行各种开源模型。如果您已遵循本章中的说明,langchain-ollama
依赖项应该已经安装;然而,我们仍然简要地介绍一下:
-
安装 LangChain Ollama 集成:
pip install langchain-ollama
-
然后拉取一个模型。从命令行,例如 bash 或 WindowsPowerShell 终端,运行:
ollama pull deepseek-r1:1.5b
-
启动 Ollama 服务器:
ollama serve
这是如何将 Ollama 与我们探索的 LCEL 模式集成的:
from langchain_ollama import ChatOllama
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Initialize Ollama with your chosen model
local_llm = ChatOllama(
model="deepseek-r1:1.5b",
temperature=0,
)
# Create an LCEL chain using the local model
prompt = PromptTemplate.from_template("Explain {concept} in simple terms")
local_chain = prompt | local_llm | StrOutputParser()
# Use the chain with your local model
result = local_chain.invoke({"concept": "quantum computing"})
print(result)
这个 LCEL 链与我们的云模型示例功能相同,展示了 LangChain 的模型无关设计。
请注意,由于您正在运行本地模型,您不需要设置任何密钥。答案非常长——尽管相当合理。您可以自己运行并查看您会得到什么答案。
现在我们已经看到了基本的文本生成,让我们看看另一个集成。Hugging Face 提供了一种易于使用的方法来本地运行模型,并可以访问庞大的预训练模型生态系统。
在本地使用 Hugging Face 模型
使用 Hugging Face,您可以选择在本地(HuggingFacePipeline)或 Hugging Face Hub(HuggingFaceEndpoint)上运行模型。在这里,我们讨论的是本地运行,因此我们将重点关注 HuggingFacePipeline
。让我们开始吧:
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline
# Create a pipeline with a small model:
llm = HuggingFacePipeline.from_model_id(
model_id="TinyLlama/TinyLlama-1.1B-Chat-v1.0",
task="text-generation",
pipeline_kwargs=dict(
max_new_tokens=512,
do_sample=False,
repetition_penalty=1.03,
),
)
chat_model = ChatHuggingFace(llm=llm)
# Use it like any other LangChain LLM
messages = [
SystemMessage(content="You're a helpful assistant"),
HumanMessage(
content="Explain the concept of machine learning in simple terms"
),
]
ai_msg = chat_model.invoke(messages)
print(ai_msg.content)
这可能需要相当长的时间,尤其是第一次,因为模型需要先下载。为了简洁,我们省略了模型响应。
LangChain 还支持通过其他集成在本地运行模型,例如:
-
llama.cpp:这个高性能的 C++实现允许在消费级硬件上高效运行基于 LLaMA 的模型。虽然我们不会详细介绍设置过程,但 LangChain 提供了与 llama.cpp 的简单集成,用于推理和微调。
-
GPT4All:GPT4All 提供轻量级模型,可以在消费级硬件上运行。LangChain 的集成使得在许多应用程序中将这些模型作为云 LLM 的即插即用替代变得容易。
当你开始使用本地模型时,你会想要优化它们的性能并处理常见的挑战。以下是一些基本的技巧和模式,这些将帮助你从 LangChain 的本地部署中获得最大收益。
本地模型的技巧
当使用本地模型时,请记住以下要点:
-
资源管理:本地模型需要仔细配置以平衡性能和资源使用。以下示例演示了如何配置 Ollama 模型以实现高效操作:
# Configure model with optimized memory and processing settings from langchain_ollama import ChatOllama llm = ChatOllama( model="mistral:q4_K_M", # 4-bit quantized model (smaller memory footprint) num_gpu=1, # Number of GPUs to utilize (adjust based on hardware) num_thread=4 # Number of CPU threads for parallel processing )
让我们看看每个参数的作用:
-
model=“mistral:q4_K_M”:指定 Mistral 模型的 4 位量化版本。量化通过使用更少的位来表示权重,以最小的精度换取显著的内存节省。例如:
-
完整精度模型:需要约 8GB RAM
-
4 位量化模型:需要约 2GB RAM
-
-
num_gpu=1:分配 GPU 资源。选项包括:
-
0:仅 CPU 模式(较慢但无需 GPU 即可工作)
-
1: 使用单个 GPU(适用于大多数桌面配置)
-
较高值:仅适用于多 GPU 系统
-
-
num_thread=4:控制 CPU 并行化:
-
较低值(2-4):适合与其他应用程序一起运行
-
较高值(8-16):在专用服务器上最大化性能
-
最佳设置:通常与 CPU 的物理核心数相匹配
-
- 错误处理:本地模型可能会遇到各种错误,从内存不足到意外的终止。一个健壮的错误处理策略是必不可少的:
def safe_model_call(llm, prompt, max_retries=2):
"""Safely call a local model with retry logic and graceful
failure"""
retries = 0
while retries <= max_retries:
try:
return llm.invoke(prompt)
except RuntimeError as e:
# Common error with local models when running out of VRAM
if "CUDA out of memory" in str(e):
print(f"GPU memory error, waiting and retrying ({retries+1}/{max_retries+1})")
time.sleep(2) # Give system time to free resources
retries += 1
else:
print(f"Runtime error: {e}")
return "An error occurred while processing your request."
except Exception as e:
print(f"Unexpected error calling model: {e}")
return "An error occurred while processing your request."
# If we exhausted retries
return "Model is currently experiencing high load. Please try again later."
# Use the safety wrapper in your LCEL chain
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableLambda
prompt = PromptTemplate.from_template("Explain {concept} in simple terms")
safe_llm = RunnableLambda(lambda x: safe_model_call(llm, x))
safe_chain = prompt | safe_llm
response = safe_chain.invoke({"concept": "quantum computing"})
你可能会遇到以下常见的本地模型错误:
-
内存不足:当模型需要的 VRAM 超过可用量时发生
-
模型加载失败:当模型文件损坏或不兼容时
-
超时问题:在资源受限的系统上推理时间过长
-
上下文长度错误:当输入超过模型的最大令牌限制时
通过实施这些优化和错误处理策略,你可以创建健壮的 LangChain 应用程序,有效地利用本地模型,即使在出现问题时也能保持良好的用户体验。
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_02_01.png
图 2.1:选择本地和基于云模型的决策图
在探讨了如何使用 LangChain 构建基于文本的应用程序之后,我们现在将扩展我们对多模态功能的理解。随着人工智能系统越来越多地与多种形式的数据一起工作,LangChain 提供了生成图像和理解视觉内容的接口——这些功能补充了我们已经涵盖的文本处理,并为更沉浸式的应用程序开辟了新的可能性。
多模态人工智能应用
人工智能系统已经超越了仅处理文本的阶段,开始处理多种数据类型。在当前环境中,我们可以区分两种关键能力,这两种能力经常被混淆,但代表了不同的技术方法。
多模态理解代表了模型能够同时处理多种类型的输入以进行推理和生成响应的能力。这些先进系统可以理解不同模态之间的关系,接受输入如文本、图像、PDF、音频、视频和结构化数据。它们的处理能力包括跨模态推理、情境感知和复杂的信息提取。Gemini 2.5、GPT-4V、Sonnet 3.7 和 Llama 4 等模型体现了这种能力。例如,一个多模态模型可以分析图表图像和文本问题,以提供关于数据趋势的见解,在单个处理流程中将视觉和文本理解结合起来。
相比之下,内容生成能力专注于创建特定类型的媒体,通常具有非凡的质量但更专业的功能。文本到图像模型从描述中创建视觉内容,文本到视频系统从提示中生成视频片段,文本到音频工具生成音乐或语音,图像到图像模型转换现有的视觉内容。例如,Midjourney、DALL-E 和 Stable Diffusion 用于图像;Sora 和 Pika 用于视频;Suno 和 ElevenLabs 用于音频。与真正的多模态模型不同,许多生成系统针对其特定的输出模态进行了专门化,即使它们可以接受多种输入类型。它们在创作方面表现出色,而不是在理解方面。
随着大型语言模型(LLMs)的发展超越文本,LangChain 正在扩展以支持多模态理解和内容生成工作流程。该框架为开发者提供了工具,使他们能够将高级功能集成到应用程序中,而无需从头开始实现复杂的集成。让我们从根据文本描述生成图像开始。LangChain 提供了多种通过外部集成和包装器实现图像生成的方法。我们将探索多种实现模式,从最简单的开始,逐步过渡到更复杂的技巧,这些技巧可以集成到您的应用程序中。
文本到图像
LangChain 与各种图像生成模型和服务集成,允许您:
-
从文本描述生成图像
-
根据文本提示编辑现有图像
-
控制图像生成参数
-
处理图像变化和风格
LangChain 包括对流行的图像生成服务的包装和模型。首先,让我们看看如何使用 OpenAI 的 DALL-E 模型系列生成图像。
通过 OpenAI 使用 DALL-E
LangChain 为 DALL-E 提供的包装简化了从文本提示生成图像的过程。该实现底层使用 OpenAI 的 API,但提供了一个与其他 LangChain 组件一致的标准化接口。
from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper
dalle = DallEAPIWrapper(
model_name="dall-e-3", # Options: "dall-e-2" (default) or "dall-e-3"
size="1024x1024", # Image dimensions
quality="standard", # "standard" or "hd" for DALL-E 3
n=1 # Number of images to generate (only for DALL-E 2)
)
# Generate an image
image_url = dalle.run("A detailed technical diagram of a quantum computer")
# Display the image in a notebook
from IPython.display import Image, display
display(Image(url=image_url))
# Or save it locally
import requests
response = requests.get(image_url)
with open("generated_library.png", "wb") as f:
f.write(response.content)
这是我们的图像:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_02_02.png
图 2.2:由 OpenAI 的 DALL-E 图像生成器生成的图像
你可能会注意到,在这些图像中的文本生成不是这些模型的优势之一。你可以在 Replicate 上找到许多图像生成模型,包括最新的 Stable Diffusion 模型,因此我们现在将使用这些模型。
使用 Stable Diffusion
Stable Diffusion 3.5 Large 是 Stability AI 在 2024 年 3 月发布的最新文本到图像模型。它是一个 多模态扩散变换器(MMDiT),能够生成具有显著细节和质量的超高分辨率图像。
此模型使用三个固定的、预训练的文本编码器,并实现了查询-键归一化以改善训练稳定性。它能够从相同的提示生成多样化的输出,并支持各种艺术风格。
from langchain_community.llms import Replicate
# Initialize the text-to-image model with Stable Diffusion 3.5 Large
text2image = Replicate(
model="stability-ai/stable-diffusion-3.5-large",
model_kwargs={
"prompt_strength": 0.85,
"cfg": 4.5,
"steps": 40,
"aspect_ratio": "1:1",
"output_format": "webp",
"output_quality": 90
}
)
# Generate an image
image_url = text2image.invoke(
"A detailed technical diagram of an AI agent"
)
新模型推荐参数包括:
-
prompt_strength:控制图像与提示的匹配程度(0.85)
-
cfg:控制模型遵循提示的严格程度(4.5)
-
steps:更多步骤会产生更高质量的图像(40)
-
aspect_ratio:设置为 1:1 以获得方形图像
-
output_format:使用 WebP 以获得更好的质量与尺寸比
-
output_quality:设置为 90 以获得高质量输出
这是我们的图像:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_02_03.png
图 2.3:由 Stable Diffusion 生成的图像
现在让我们探索如何使用多模态模型分析和理解图像。
图像理解
图像理解指的是人工智能系统以类似于人类视觉感知的方式解释和分析视觉信息的能力。与传统的计算机视觉(专注于特定任务,如目标检测或人脸识别)不同,现代多模态模型可以对图像进行一般推理,理解上下文、关系,甚至视觉内容中的隐含意义。
Gemini 2.5 Pro 和 GPT-4 Vision 等模型可以分析图像并提供详细的描述或回答有关它们的问题。
使用 Gemini 1.5 Pro
LangChain 通过相同的 ChatModel
接口处理多模态输入。它接受 Messages
作为输入,一个 Message
对象有一个 content
字段。IA content
可以由多个部分组成,每个部分可以代表不同的模态(这允许你在提示中混合不同的模态)。
你可以通过值或引用发送多模态输入。要按值发送,你应该将字节编码为字符串,并构建一个格式如下所示的image_url
变量,使用我们使用 Stable Diffusion 生成的图像:
import base64
from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
from langchain_core.messages.human import HumanMessage
with open("stable-diffusion.png", 'rb') as image_file:
image_bytes = image_file.read()
base64_bytes = base64.b64encode(image_bytes).decode("utf-8")
prompt = [
{"type": "text", "text": "Describe the image: "},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_bytes}"}},
]
llm = ChatGoogleGenerativeAI(
model="gemini-1.5-pro",
temperature=0,
)
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)
The image presents a futuristic, stylized depiction of a humanoid robot's upper body against a backdrop of glowing blue digital displays. The robot's head is rounded and predominantly white, with sections of dark, possibly metallic, material around the face and ears. The face itself features glowing orange eyes and a smooth, minimalist design, lacking a nose or mouth in the traditional human sense. Small, bright dots, possibly LEDs or sensors, are scattered across the head and body, suggesting advanced technology and intricate construction.
The robot's neck and shoulders are visible, revealing a complex internal structure of dark, interconnected parts, possibly wires or cables, which contrast with the white exterior. The shoulders and upper chest are also white, with similar glowing dots and hints of the internal mechanisms showing through. The overall impression is of a sleek, sophisticated machine.
The background is a grid of various digital interfaces, displaying graphs, charts, and other abstract data visualizations. These elements are all in shades of blue, creating a cool, technological ambiance that complements the robot's appearance. The displays vary in size and complexity, adding to the sense of a sophisticated control panel or monitoring system. The combination of the robot and the background suggests a theme of advanced robotics, artificial intelligence, or data analysis.
由于多模态输入通常具有很大的体积,将原始字节作为请求的一部分发送可能不是最佳选择。你可以通过指向 blob 存储来按引用发送它,但具体的存储类型取决于模型的提供者。例如,Gemini 接受多媒体输入作为对 Google Cloud Storage 的引用——这是由 Google Cloud 提供的一个 blob 存储服务。
prompt = [
{"type": "text", "text": "Describe the video in a few sentences."},
{"type": "media", "file_uri": video_uri, "mime_type": "video/mp4"},
]
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)
如何构建多模态输入的详细说明可能取决于 LLM 的提供者(以及相应的 LangChain 集成相应地处理content
字段的一部分的字典)。例如,Gemini 接受一个额外的"video_metadata"
键,可以指向要分析的视频片段的开始和/或结束偏移量:
offset_hint = {
"start_offset": {"seconds": 10},
"end_offset": {"seconds": 20},
}
prompt = [
{"type": "text", "text": "Describe the video in a few sentences."},
{"type": "media", "file_uri": video_uri, "mime_type": "video/mp4", "video_metadata": offset_hint},
]
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)
当然,这样的多模态部分也可以进行模板化。让我们用一个简单的模板来演示,该模板期望一个包含编码字节的image_bytes_str
参数:
prompt = ChatPromptTemplate.from_messages(
[("user",
[{"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,{image_bytes_str}"},
}])]
)
prompt.invoke({"image_bytes_str": "test-url"})
使用 GPT-4 Vision
在探索了图像生成之后,让我们看看 LangChain 如何使用多模态模型处理图像理解。GPT-4 Vision 功能(在 GPT-4o 和 GPT-4o-mini 等模型中可用)使我们能够在文本旁边分析图像,使能够“看到”并对视觉内容进行推理的应用成为可能。
LangChain 通过提供多模态输入的一致接口简化了与这些模型的工作。让我们实现一个灵活的图像分析器:
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
def analyze_image(image_url: str, question: str) -> str:
chat = ChatOpenAI(model="gpt-4o-mini", max_tokens=256)
message = HumanMessage(
content=[
{
"type": "text",
"text": question
},
{
"type": "image_url",
"image_url": {
"url": image_url,
"detail": "auto"
}
}
]
)
response = chat.invoke([message])
return response.content
# Example usage
image_url = "https://replicate.delivery/yhqm/pMrKGpyPDip0LRciwSzrSOKb5ukcyXCyft0IBElxsT7fMrLUA/out-0.png"
questions = [
"What objects do you see in this image?",
"What is the overall mood or atmosphere?",
"Are there any people in the image?"
]
for question in questions:
print(f"\nQ: {question}")
print(f"A: {analyze_image(image_url, question)}")
该模型为我们生成的城市景观提供了丰富、详细的分析:
Q: What objects do you see in this image?
A: The image features a futuristic cityscape with tall, sleek skyscrapers. The buildings appear to have a glowing or neon effect, suggesting a high-tech environment. There is a large, bright sun or light source in the sky, adding to the vibrant atmosphere. A road or pathway is visible in the foreground, leading toward the city, possibly with light streaks indicating motion or speed. Overall, the scene conveys a dynamic, otherworldly urban landscape.
Q: What is the overall mood or atmosphere?
A: The overall mood or atmosphere of the scene is futuristic and vibrant. The glowing outlines of the skyscrapers and the bright sunset create a sense of energy and possibility. The combination of deep colors and light adds a dramatic yet hopeful tone, suggesting a dynamic and evolving urban environment.
Q: Are there any people in the image?
A: There are no people in the image. It appears to be a futuristic cityscape with tall buildings and a sunset.
这种能力为 LangChain 应用开辟了众多可能性。通过将图像分析与我们在本章早期探索的文本处理模式相结合,你可以构建跨模态推理的复杂应用。在下一章中,我们将在此基础上创建更复杂的多模态应用。
摘要
在设置我们的开发环境并配置必要的 API 密钥后,我们已经探索了 LangChain 开发的基础,从基本链到多模态功能。我们看到了 LCEL 如何简化复杂的工作流程,以及 LangChain 如何与文本和图像处理集成。这些构建块为我们下一章中更高级的应用做好了准备。
在下一章中,我们将扩展这些概念,以创建具有增强控制流、结构化输出和高级提示技术的更复杂的多模态应用。你将学习如何将多种模态结合到复杂的链中,整合更复杂的错误处理,并构建充分利用现代 LLM 全部潜力的应用。
复习问题
-
LangChain 解决了原始 LLM 的哪三个主要限制?
-
内存限制
-
工具集成
-
上下文约束
-
处理速度
-
成本优化
-
-
以下哪项最能描述 LCEL (LangChain 表达语言) 的目的?
-
LLM 的编程语言
-
组成 LangChain 组件的统一接口
-
提示模板系统
-
LLMs 的测试框架
-
-
列出 LangChain 中可用的三种内存系统类型
-
比较 LangChain 中的 LLMs 和聊天模型,它们的接口和使用案例有何不同?
-
Runnables 在 LangChain 中扮演什么角色?它们如何有助于构建模块化的 LLM 应用程序?
-
当在本地运行模型时,哪些因素会影响模型性能?(选择所有适用的)
-
可用 RAM
-
CPU/GPU 功能
-
互联网连接速度
-
模型量化级别
-
操作系统类型
-
-
比较以下模型部署选项,并确定每个选项最合适的场景:
-
基于云的模型(例如,OpenAI)
-
使用 llama.cpp 的本地模型
-
GPT4All 集成
-
-
使用 LCEL 设计一个基本的链,该链将:
-
针对一个产品的用户问题进行提问
-
查询数据库以获取产品信息
-
使用 LLM 生成响应
-
-
提供一个概述组件及其连接方式的草图。
-
比较以下图像分析方法,并提及它们之间的权衡:
-
方法 A
from langchain_openai import ChatOpenAI chat = ChatOpenAI(model="gpt-4-vision-preview")
-
方法 B
from langchain_community.llms import Ollama local_model = Ollama(model="llava")
-
订阅我们的每周通讯简报
订阅 AI_Distilled,这是 AI 专业人士、研究人员和创新者的首选通讯简报,请访问 packt.link/Q5UyU
。
第四章:使用 LangGraph 构建工作流程
到目前为止,我们已经了解了 LLMs、LangChain 作为框架,以及如何在纯模式(仅基于提示生成文本输出)下使用 LangChain 与 LLMs 结合。在本章中,我们将从 LangGraph 作为框架的快速介绍开始,并探讨如何通过连接多个步骤来使用 LangChain 和 LangGraph 开发更复杂的工作流程。作为一个例子,我们将讨论解析 LLM 输出,并使用 LangChain 和 LangGraph 探讨错误处理模式。然后,我们将继续探讨开发提示的更高级方法,并探索 LangChain 为少样本提示和其他技术提供的构建块。
我们还将介绍如何处理多模态输入,利用长上下文,以及调整工作负载以克服与上下文窗口大小相关的限制。最后,我们将探讨使用 LangChain 管理内存的基本机制。理解这些基本和关键技术将帮助我们阅读 LangGraph 代码,理解教程和代码示例,并开发我们自己的复杂工作流程。当然,我们还将讨论 LangGraph 工作流程是什么,并在第五章和第六章中继续构建这一技能。
简而言之,在本章中,我们将涵盖以下主要主题:
-
LangGraph 基础知识
-
提示工程
-
与短上下文窗口一起工作
-
理解内存机制
如往常一样,您可以在我们的公共 GitHub 仓库中找到所有代码示例,作为 Jupyter 笔记本:github.com/benman1/generative_ai_with_langchain/tree/second_edition/chapter3
。
LangGraph 基础知识
LangGraph 是由 LangChain(作为一家公司)开发的一个框架,它有助于控制和编排工作流程。为什么我们需要另一个编排框架呢?让我们把这个问题放在一边,直到第五章(E_Chapter_5.xhtml#_idTextAnchor231),在那里我们将触及代理和代理工作流程,但现在,让我们提到 LangGraph 作为编排框架的灵活性及其在处理复杂场景中的稳健性。
与许多其他框架不同,LangGraph 允许循环(大多数其他编排框架仅使用直接无环图),支持开箱即用的流式处理,并且有许多预构建的循环和组件,专门用于生成式 AI 应用(例如,人工审核)。LangGraph 还有一个非常丰富的 API,允许您在需要时对执行流程进行非常细粒度的控制。这在我们书中并未完全涵盖,但请记住,如果您需要,您始终可以使用更底层的 API。
有向无环图(DAG)是图论和计算机科学中的一种特殊类型的图。它的边(节点之间的连接)有方向,这意味着从节点 A 到节点 B 的连接与从节点 B 到节点 A 的连接不同。它没有环。换句话说,没有路径可以从一个节点开始,通过跟随有向边返回到同一个节点。
在数据工程中,DAG(有向无环图)通常用作工作流程的模型,其中节点是任务,边是这些任务之间的依赖关系。例如,从节点 A 到节点 B 的边意味着我们需要从节点 A 获取输出以执行节点 B。
目前,让我们从基础知识开始。如果你是框架的新手,我们强烈推荐参加一个免费的在线课程,该课程可在academy.langchain.com/
找到,以加深你对 LangGraph 的理解。
状态管理
在现实世界的 AI 应用中,状态管理至关重要。例如,在一个客户服务聊天机器人中,状态可能会跟踪客户 ID、对话历史和未解决的问题等信息。LangGraph 的状态管理让你能够在多个 AI 组件的复杂工作流程中维护这个上下文。
LangGraph 允许你开发和执行复杂的称为图的工作流程。在本章中,我们将交替使用图和工作流程这两个词。一个图由节点及其之间的边组成。节点是工作流程的组成部分,而工作流程有一个状态。那是什么意思呢?首先,状态通过跟踪用户输入和之前的计算来使节点意识到当前上下文。其次,状态允许你在任何时间点持久化你的工作流程执行。第三,状态使你的工作流程真正交互式,因为一个节点可以通过更新状态来改变工作流程的行为。为了简单起见,可以把状态想象成一个 Python 字典。节点是操作这个字典的 Python 函数。它们接受一个字典作为输入,并返回另一个包含要更新工作流程状态的键和值的字典。
让我们用一个简单的例子来理解这一点。首先,我们需要定义一个状态的模式:
from typing_extensions import TypedDict
class JobApplicationState(TypedDict):
job_description: str
is_suitable: bool
application: str
TypedDict
是一个 Python 类型构造函数,允许定义具有预定义键集的字典,每个键都可以有自己的类型(与Dict[str, str]
构造相反)。
LangGraph 状态的模式不一定必须定义为TypedDict
;你也可以使用数据类或 Pydantic 模型。
在我们定义了状态的模式之后,我们可以定义我们的第一个简单工作流程:
from langgraph.graph import StateGraph, START, END, Graph
def analyze_job_description(state):
print("...Analyzing a provided job description ...")
return {"is_suitable": len(state["job_description"]) > 100}
def generate_application(state):
print("...generating application...")
return {"application": "some_fake_application"}
builder = StateGraph(JobApplicationState)
builder.add_node("analyze_job_description", analyze_job_description)
builder.add_node("generate_application", generate_application)
builder.add_edge(START, "analyze_job_description")
builder.add_edge("analyze_job_description", "generate_application")
builder.add_edge("generate_application", END)
graph = builder.compile()
在这里,我们定义了两个 Python 函数,它们是我们工作流程的组成部分。然后,我们通过提供状态的模式,在它们之间添加节点和边来定义我们的工作流程。add_node
是一种方便的方法,可以将组件添加到您的图中(通过提供其名称和相应的 Python 函数),您可以在定义边时使用add_edge
引用此名称。START
和END
是保留的内置节点,分别定义工作流程的开始和结束。
让我们通过使用内置的可视化机制来查看我们的工作流程:
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_03_01.png
图 3.1:LangGraph 内置可视化我们的第一个工作流程
我们的功能通过简单地从 LangGraph 自动提供的输入字典中读取来访问状态。LangGraph 隔离状态更新。当一个节点接收到状态时,它得到一个不可变的副本,而不是实际状态对象的引用。节点必须返回一个包含它想要更新的特定键和值的字典。LangGraph 然后将这些更新合并到主状态中。这种模式防止了副作用,并确保状态更改是明确和可追踪的。
节点修改状态的唯一方式是提供一个包含要更新的键值对的输出字典,LangGraph 将处理它。节点至少应该修改状态中的一个键。graph
实例本身就是一个Runnable
(更准确地说,它继承自Runnable
),我们可以执行它。我们应该提供一个包含初始状态的字典,我们将得到最终状态作为输出:
res = graph.invoke({"job_description":"fake_jd"})
print(res)
>>...Analyzing a provided job description ...
...generating application...
{'job_description': 'fake_jd', 'is_suitable': True, 'application': 'some_fake_application'}
我们使用一个非常简单的图作为示例。对于您的实际工作流程,您可以定义并行步骤(例如,您可以轻松地将一个节点与多个节点连接起来)甚至循环。LangGraph 通过所谓的超级步骤执行工作流程,这些步骤可以同时调用多个节点(然后合并这些节点的状态更新)。您可以在图中控制递归深度和总的超级步骤数量,这有助于您避免循环无限运行,尤其是在 LLMs 的输出非确定性时。
LangGraph 上的超级步骤代表对一或几个节点的离散迭代,它受到了 Google 构建的用于大规模处理大型图的系统 Pregel 的启发。它处理节点的并行执行和发送到中央图状态的状态更新。
在我们的示例中,我们使用了从节点到另一个节点的直接边。这使得我们的图与我们可以用 LangChain 定义的顺序链没有区别。LangGraph 的一个关键特性是能够创建条件边,这些边可以根据当前状态将执行流程导向一个或另一个节点。条件边是一个 Python 函数,它接收当前状态作为输入,并返回一个包含要执行节点的名称的字符串。
让我们来看一个例子:
from typing import Literal
builder = StateGraph(JobApplicationState)
builder.add_node("analyze_job_description", analyze_job_description)
builder.add_node("generate_application", generate_application)
def is_suitable_condition(state: StateGraph) -> Literal["generate_application", END]:
if state.get("is_suitable"):
return "generate_application"
return END
builder.add_edge(START, "analyze_job_description")
builder.add_conditional_edges("analyze_job_description", is_suitable_condition)
builder.add_edge("generate_application", END)
graph = builder.compile()
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
我们定义了一个边缘 is_suitable_condition
,它通过分析当前状态来接收一个状态并返回一个 END
或 generate_application
字符串。我们使用了 Literal
类型提示,因为 LangGraph 使用它来确定在创建条件边缘时将源节点连接到哪些目标节点。如果你不使用类型提示,你可以直接向 add_conditional_edges
函数提供一个目标节点列表;否则,LangGraph 将将源节点连接到图中所有其他节点(因为它在创建图时不会分析边缘函数的代码)。以下图显示了生成的输出:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_03_02.png
图 3.2:具有条件边缘的工作流程(用虚线表示)
条件边缘用虚线表示,现在我们可以看到,根据 analyze_job_description
步骤的输出,我们的图可以执行不同的操作。
还原器
到目前为止,我们的节点通过更新对应键的值来改变状态。从另一个角度来看,在每次超级步骤中,LangGraph 可以为给定的键生成一个新的值。换句话说,对于状态中的每个键,都有一个值的序列,并且从函数式编程的角度来看,可以将 reduce
函数应用于这个序列。LangGraph 上的默认还原器始终用新值替换最终值。让我们想象我们想要跟踪自定义操作(由节点产生)并比较三个选项。
第一种选择是,节点应该返回一个列表作为 actions
键的值。我们只提供简短的代码示例以供说明,但你可以从 Github 上找到完整的示例。如果这样的值已经存在于状态中,它将被新的一个所取代:
class JobApplicationState(TypedDict):
...
actions: list[str]
另一个选项是使用带有 Annotated
类型提示的默认 add
方法。通过使用此类型提示,我们告诉 LangGraph 编译器状态中变量的类型是字符串列表,并且它应该使用 add
方法将两个列表连接起来(如果值已经存在于状态中并且节点产生了一个新的值):
from typing import Annotated, Optional
from operator import add
class JobApplicationState(TypedDict):
...
actions: Annotated[list[str], add]
最后一个选项是编写自己的自定义还原器。在这个例子中,我们编写了一个自定义还原器,它不仅接受来自节点的列表(作为新值),还接受一个将被转换为列表的单个字符串:
from typing import Annotated, Optional, Union
def my_reducer(left: list[str], right: Optional[Union[str, list[str]]]) -> list[str]:
if right:
return left + [right] if isinstance(right, str) else left + right
return left
class JobApplicationState(TypedDict):
...
actions: Annotated[list[str], my_reducer]
LangGraph 有几个内置的还原器,我们还将演示如何实现自己的还原器。其中之一是 add_messages
,它允许我们合并消息。许多节点将是 LLM 代理,而 LLM 通常与消息一起工作。因此,根据我们在第五章和第六章中将更详细讨论的对话编程范式,你通常需要跟踪这些消息:
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
class JobApplicationState(TypedDict):
...
messages: Annotated[list[AnyMessage], add_messages]
由于这是一个如此重要的还原器,因此有一个内置的状态你可以继承:
from langgraph.graph import MessagesState
class JobApplicationState(MessagesState):
...
现在,既然我们已经讨论了 reducers,让我们谈谈对任何开发者都非常重要的另一个概念——如何通过传递配置来编写可重用和模块化的工作流。
使图形可配置
LangGraph 提供了一个强大的 API,允许您使您的图形可配置。它允许您将参数与用户输入分离——例如,在不同的 LLM 提供商之间进行实验或传递自定义回调。一个节点也可以通过接受它作为第二个参数来访问配置。配置将以RunnableConfig
实例的形式传递。
RunnableConfig
是一个类型化字典,它让您可以控制执行控制设置。例如,您可以使用recursion_limit
参数控制最大超步数。RunnableConfig
还允许您在configurable
键下作为单独的字典传递自定义参数。
让我们的节点在应用程序生成期间使用不同的 LLM:
from langchain_core.runnables.config import RunnableConfig
def generate_application(state: JobApplicationState, config: RunnableConfig):
model_provider = config["configurable"].get("model_provider", "Google")
model_name = config["configurable"].get("model_name", "gemini-1.5-flash-002")
print(f"...generating application with {model_provider} and {model_name} ...")
return {"application": "some_fake_application", "actions": ["action2", "action3"]}
现在让我们使用自定义配置(如果您不提供任何配置,LangGraph 将使用默认配置)编译和执行我们的图形:
res = graph.invoke({"job_description":"fake_jd"}, config={"configurable": {"model_provider": "OpenAI", "model_name": "gpt-4o"}})
print(res)
>> ...Analyzing a provided job description ...
...generating application with OpenAI and OpenAI ...
{'job_description': 'fake_jd', 'is_suitable': True, 'application': 'some_fake_application', 'actions': ['action1', 'action2', 'action3']}
既然我们已经建立了如何使用 LangGraph 构建复杂工作流的方法,让我们看看这些工作流面临的一个常见挑战:确保 LLM 的输出符合下游组件所需的精确结构。强大的输出解析和优雅的错误处理对于可靠的 AI 管道至关重要。
受控输出生成
当您开发复杂的工作流时,您需要解决的一个常见任务是强制 LLM 生成遵循特定结构的输出。这被称为受控生成。这样,它可以由工作流中更进一步的步骤以编程方式消费。例如,我们可以要求 LLM 为 API 调用生成 JSON 或 XML,从文本中提取某些属性,或生成 CSV 表格。有多种方法可以实现这一点,我们将在本章开始探索它们,并在第五章中继续讨论。由于 LLM 可能并不总是遵循确切的输出结构,下一步可能会失败,您需要从错误中恢复。因此,我们还将开始在本节中讨论错误处理。
输出解析
当将 LLM 集成到更大的工作流中时,输出解析至关重要,因为后续步骤需要结构化数据而不是自然语言响应。一种方法是向提示中添加相应的指令并解析输出。
让我们看看一个简单的任务。我们希望将某个工作描述是否适合初级 Java 程序员作为我们管道的一个步骤进行分类,并根据 LLM 的决定,我们希望继续申请或忽略这个具体的工作描述。我们可以从一个简单的提示开始:
from langchain_google_vertexai import ChatVertexAI
llm = ChatVertexAI(model="gemini-1.5-flash-002")
job_description: str = ... # put your JD here
prompt_template = (
"Given a job description, decide whether it suits a junior Java developer."
"\nJOB DESCRIPTION:\n{job_description}\n"
)
result = llm.invoke(prompt_template.format(job_description=job_description))
print(result.content)
>> No, this job description is not suitable for a junior Java developer.\n\nThe key reasons are:\n\n* … (output reduced)
如您所见,LLM 的输出是自由文本,这可能在后续的管道步骤中难以解析或解释。如果我们向提示中添加一个特定的指令会怎样呢?
prompt_template_enum = (
"Given a job description, decide whether it suits a junior Java developer."
"\nJOB DESCRIPTION:\n{job_description}\n\nAnswer only YES or NO."
)
result = llm.invoke(prompt_template_enum.format(job_description=job_description))
print(result.content)
>> NO
现在,我们如何解析这个输出?当然,我们的下一步可以是简单地查看文本并根据字符串比较进行条件判断。但这对于更复杂的使用案例不起作用——例如,如果下一步期望输出是一个 JSON 对象。为了处理这种情况,LangChain 提供了大量的 OutputParsers,它们可以接受 LLM 生成的输出并将其尝试解析为所需的格式(如果需要,则检查模式)——列表、CSV、枚举、pandas DataFrame、Pydantic 模型、JSON、XML 等等。每个解析器都实现了 BaseGenerationOutputParser
接口,该接口扩展了 Runnable
接口并添加了一个额外的 parse_result
方法。
让我们构建一个解析器,将输出解析为枚举:
from enum import Enum
from langchain.output_parsers import EnumOutputParser
from langchain_core.messages import HumanMessage
class IsSuitableJobEnum(Enum):
YES = "YES"
NO = "NO"
parser = EnumOutputParser(enum=IsSuitableJobEnum)
assert parser.invoke("NO") == IsSuitableJobEnum.NO
assert parser.invoke("YES\n") == IsSuitableJobEnum.YES
assert parser.invoke(" YES \n") == IsSuitableJobEnum.YES
assert parser.invoke(HumanMessage(content="YES")) == IsSuitableJobEnum.YES
EnumOutputParser
将文本输出转换为相应的 Enum
实例。请注意,解析器处理任何类似生成的输出(不仅仅是字符串),并且实际上它还会去除输出。
你可以在文档中找到完整的解析器列表,链接为 python.langchain.com/docs/concepts/output_parsers/
,如果你需要自己的解析器,你总是可以构建一个新的!
作为最后一步,让我们将所有内容组合成一个链:
chain = llm | parser
result = chain.invoke(prompt_template_enum.format(job_description=job_description))
print(result)
>> NO
现在,让我们将这个链作为我们 LangGraph 工作流程的一部分:
class JobApplicationState(TypedDict):
job_description: str
is_suitable: IsSuitableJobEnum
application: str
analyze_chain = llm | parser
def analyze_job_description(state):
prompt = prompt_template_enum.format(job_description=state["job_description"])
result = analyze_chain.invoke(prompt)
return {"is_suitable": result}
def is_suitable_condition(state: StateGraph):
return state["is_suitable"] == IsSuitableJobEnum.YES
builder = StateGraph(JobApplicationState)
builder.add_node("analyze_job_description", analyze_job_description)
builder.add_node("generate_application", generate_application)
builder.add_edge(START, "analyze_job_description")
builder.add_conditional_edges(
"analyze_job_description", is_suitable_condition,
{True: "generate_application", False: END})
builder.add_edge("generate_application", END)
我们做出了两个重要的更改。首先,我们新构建的链现在是表示 analyze_job_description
节点的 Python 函数的一部分,这就是我们在节点内实现逻辑的方式。其次,我们的条件边函数不再返回一个字符串,而是我们在 add_conditional_edges
函数中添加了返回值到目标边的映射,这是一个如何实现工作流程分支的例子。
让我们花些时间讨论如果我们的解析失败,如何处理潜在的错误!
错误处理
在任何 LangChain 工作流程中,有效的错误管理都是必不可少的,包括处理工具故障(我们将在 第五章 中探讨,当我们到达工具时)。在开发 LangChain 应用程序时,请记住,失败可能发生在任何阶段:
-
调用基础模型的 API 可能会失败
-
LLM 可能会生成意外的输出
-
外部服务可能会不可用
可能的方法之一是使用基本的 Python 机制来捕获异常,将其记录以供进一步分析,并通过将异常包装为文本或返回默认值来继续你的工作流程。如果你的 LangChain 链调用某些自定义 Python 函数,请考虑适当的异常处理。同样适用于你的 LangGraph 节点。
记录日志是至关重要的,尤其是在你接近生产部署时。适当的日志记录确保异常不会被忽略,从而允许你监控其发生。现代可观察性工具提供警报机制,可以分组类似错误并通知你关于频繁发生的问题。
将异常转换为文本使您的流程能够在提供有关出错情况和潜在恢复路径的有价值上下文的同时继续执行。以下是一个简单的示例,说明您如何记录异常但通过坚持默认行为继续执行您的流程:
import logging
logger = logging.getLogger(__name__)
llms = {
"fake": fake_llm,
"Google": llm
}
def analyze_job_description(state, config: RunnableConfig):
try:
llm = config["configurable"].get("model_provider", "Google")
llm = llms[model_provider]
analyze_chain = llm | parser
prompt = prompt_template_enum.format(job_description=job_description)
result = analyze_chain.invoke(prompt)
return {"is_suitable": result}
except Exception as e:
logger.error(f"Exception {e} occurred while executing analyze_job_description")
return {"is_suitable": False}
为了测试我们的错误处理,我们需要模拟 LLM 失败。LangChain 有几个 FakeChatModel
类可以帮助您测试您的链:
-
GenericFakeChatModel
根据提供的迭代器返回消息 -
FakeChatModel
总是返回一个"fake_response"
字符串 -
FakeListChatModel
接收一条消息列表,并在每次调用时逐个返回它们
让我们创建一个每两次失败一次的假 LLM:
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
class MessagesIterator:
def __init__(self):
self._count = 0
def __iter__(self):
return self
def __next__(self):
self._count += 1
if self._count % 2 == 1:
raise ValueError("Something went wrong")
return AIMessage(content="False")
fake_llm = GenericFakeChatModel(messages=MessagesIterator())
当我们将此提供给我们的图(完整的代码示例可在我们的 GitHub 仓库中找到)时,我们可以看到即使在遇到异常的情况下,工作流程也会继续:
res = graph.invoke({"job_description":"fake_jd"}, config={"configurable": {"model_provider": "fake"}})
print(res)
>> ERROR:__main__:Exception Expected a Runnable, callable or dict.Instead got an unsupported type: <class 'str'> occured while executing analyze_job_description
{'job_description': 'fake_jd', 'is_suitable': False}
当发生错误时,有时再次尝试可能会有所帮助。LLM 具有非确定性,下一次尝试可能会成功;此外,如果您正在使用第三方 API,提供商的侧可能会有各种失败。让我们讨论如何使用 LangGraph 实现适当的重试。
重试
有三种不同的重试方法,每种方法都适合不同的场景:
-
使用 Runnable 的通用重试
-
节点特定的重试策略
-
语义输出修复
让我们逐一查看这些内容,从每个 Runnable
可用的通用重试开始。
您可以使用内置机制重试任何 Runnable
或 LangGraph 节点:
fake_llm_retry = fake_llm.with_retry(
retry_if_exception_type=(ValueError,),
wait_exponential_jitter=True,
stop_after_attempt=2,
)
analyze_chain_fake_retries = fake_llm_retry | parser
使用 LangGraph,您还可以为每个节点描述特定的重试。例如,让我们在发生 ValueError
的情况下重试我们的 analyze_job_description
节点两次:
from langgraph.pregel import RetryPolicy
builder.add_node(
"analyze_job_description", analyze_job_description,
retry=RetryPolicy(retry_on=ValueError, max_attempts=2))
您正在使用的组件,通常称为构建块,可能有自己的重试机制,该机制通过向 LLM 提供有关出错情况的信息来尝试算法性地修复问题。例如,LangChain 上的许多聊天模型在特定的服务器端错误上具有客户端重试。
ChatAnthropic 有一个 max_retries
参数,您可以在每个实例或每个请求中定义。另一个更高级的构建块示例是尝试从解析错误中恢复。重试解析步骤通常没有帮助,因为通常解析错误与不完整的 LLM 输出有关。如果我们重试生成步骤并寄希望于最好的结果,或者实际上给 LLM 提供有关出错情况的提示呢?这正是 RetryWithErrorOutputParser
所做的。
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_03_03.png
图 3.3:向具有多个步骤的链添加重试机制
为了使用 RetryWithErrorOutputParser
,我们首先需要用 LLM(用于修复输出)和我们的解析器来初始化它。然后,如果我们的解析失败,我们运行它并提供我们的初始提示(包含所有替换参数)、生成的响应和解析错误:
from langchain.output_parsers import RetryWithErrorOutputParser
fix_parser = RetryWithErrorOutputParser.from_llm(
llm=llm, # provide llm here
parser=parser, # your original parser that failed
prompt=retry_prompt, # an optional parameter, you can redefine the default prompt
)
fixed_output = fix_parser.parse_with_prompt(
completion=original_response, prompt_value=original_prompt)
我们可以在 GitHub 上阅读源代码以更好地理解发生了什么,但本质上,这是一个没有太多细节的伪代码示例。我们展示了如何将解析错误和导致此错误的原输出传递回 LLM,并要求它修复问题:
prompt = """
Prompt: {prompt} Completion: {completion} Above, the Completion did not satisfy the constraints given in the Prompt. Details: {error} Please try again:
"""
retry_chain = prompt | llm | StrOutputParser()
# try to parse a completion with a provided parser
parser.parse(completion)
# if it fails, catch an error and try to recover max_retries attempts
completion = retry_chain.invoke(original_prompt, completion, error)
我们在第二章中介绍了 StrOutputParser
,用于将 ChatModel 的输出从 AIMessage 转换为字符串,这样我们就可以轻松地将它传递到链中的下一步。
另一点需要记住的是,LangChain 的构建块允许你重新定义参数,包括默认提示。你总是可以在 GitHub 上检查它们;有时为你的工作流程自定义默认提示是个好主意。
您可以在此处了解其他可用的输出修复解析器:python.langchain.com/docs/how_to/output_parser_retry/
。
回退
在软件开发中,回退是一个备选程序,允许你在基本程序失败时恢复。LangChain 允许你在 Runnable
级别定义回退。如果执行失败,将触发一个具有相同输入参数的替代链。例如,如果你使用的 LLM 在短时间内不可用,你的链将自动切换到使用替代提供者(可能还有不同的提示)的另一个链。
我们的假模型每秒失败一次,所以让我们给它添加一个回退。它只是一个打印语句的 lambda 函数。正如我们所看到的,每秒都会执行回退:
from langchain_core.runnables import RunnableLambda
chain_fallback = RunnableLambda(lambda _: print("running fallback"))
chain = fake_llm | RunnableLambda(lambda _: print("running main chain"))
chain_with_fb = chain.with_fallbacks([chain_fallback])
chain_with_fb.invoke("test")
chain_with_fb.invoke("test")
>> running fallback
running main chain
生成可以遵循特定模板且可以可靠解析的复杂结果称为结构化生成(或受控生成)。这有助于构建更复杂的流程,其中 LLM 驱动的步骤的输出可以被另一个程序性步骤消费。我们将在第五章和第六章中更详细地介绍这一点。
发送到 LLM 的提示是您工作流程中最重要的构建块之一。因此,让我们接下来讨论一些提示工程的基本知识,并看看如何使用 LangChain 组织您的提示。
提示工程
让我们继续探讨提示工程,并探索与它相关的各种 LangChain 语法。但首先,让我们讨论提示工程与提示设计之间的区别。这些术语有时被互换使用,这造成了一定程度的混淆。正如我们在第一章中讨论的那样,关于 LLMs 的一个重大发现是它们具有通过上下文学习进行领域适应的能力。通常,仅用自然语言描述我们希望它执行的任务就足够了,即使 LLM 没有在这个特定任务上接受过训练,它也能表现出极高的性能。但正如我们可以想象的那样,描述同一任务的方式有很多种,LLMs 对这一点很敏感。为了提高特定任务上的性能而改进我们的提示(或更具体地说,提示模板)被称为提示工程。然而,开发更通用的提示,以引导 LLMs 在广泛的任务集上生成更好的响应,被称为提示设计。
存在着大量不同的提示工程技术。我们在这个部分不会详细讨论许多技术,但我们会简要介绍其中的一些,以展示 LangChain 的关键功能,这些功能将允许你构建任何想要的提示。
你可以在 Sander Schulhoff 及其同事发表的论文《提示报告:提示工程技术的系统调查》中找到一个关于提示分类学的良好概述:arxiv.org/abs/2406.06608
。
提示模板
在第二章中我们所做的是被称为零样本提示的工作。我们创建了一个包含每个任务描述的提示模板。当我们运行工作流程时,我们会用运行时参数替换这个提示模板中的某些值。LangChain 有一些非常有用的抽象方法来帮助完成这项工作。
在第二章中,我们介绍了PromptTemplate
,它是一个RunnableSerializable
。记住,它在调用时替换一个字符串模板——例如,你可以基于 f-string 创建一个模板并添加你的链,LangChain 会从输入中传递参数,在模板中替换它们,并将字符串传递到链的下一步:
from langchain_core.output_parsers import StrOutputParser
lc_prompt_template = PromptTemplate.from_template(prompt_template)
chain = lc_prompt_template | llm | StrOutputParser()
chain.invoke({"job_description": job_description})
对于聊天模型,输入不仅可以是一个字符串,还可以是messages
的列表——例如,一个系统消息后跟对话的历史记录。因此,我们也可以创建一个准备消息列表的模板,模板本身可以基于消息列表或消息模板创建,如下例所示:
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage, HumanMessage
msg_template = HumanMessagePromptTemplate.from_template(
prompt_template)
msg_example = msg_template.format(job_description="fake_jd")
chat_prompt_template = ChatPromptTemplate.from_messages([
SystemMessage(content="You are a helpful assistant."),
msg_template])
chain = chat_prompt_template | llm | StrOutputParser()
chain.invoke({"job_description": job_description})
你也可以更方便地完成同样的工作,而不使用聊天提示模板,只需提交一个包含消息类型和模板字符串的元组(因为有时它更快更方便):
chat_prompt_template = ChatPromptTemplate.from_messages(
[("system", "You are a helpful assistant."),
("human", prompt_template)])
另一个重要的概念是占位符。它用实时提供的消息列表替换变量。你可以通过使用placeholder
提示或添加MessagesPlaceholder
来将占位符添加到提示中。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
chat_prompt_template = ChatPromptTemplate.from_messages(
[("system", "You are a helpful assistant."),
("placeholder", "{history}"),
# same as MessagesPlaceholder("history"),
("human", prompt_template)])
len(chat_prompt_template.invoke({"job_description": "fake", "history": [("human", "hi!"), ("ai", "hi!")]}).messages)
>> 4
现在我们的输入由四条消息组成——一条系统消息,两条我们提供的历史消息,以及一条来自模板提示的人类消息。使用占位符的最佳例子是输入聊天历史,但我们在本书后面的章节中将会看到更高级的例子,届时我们将讨论 LLM 如何与外部世界互动,或者不同的 LLM 如何在多智能体设置中协同工作。
零样本提示与少样本提示
正如我们之前讨论的,我们首先想要实验的是改进任务描述本身。没有解决方案示例的任务描述被称为零样本提示,你可以尝试多种技巧。
通常有效的方法是为 LLM 分配一个特定的角色(例如,“你是为 XXX 财富 500 强公司工作的有用企业助理”)并给出一些额外的指令(例如,LLM 是否应该具有创造性、简洁或事实性)。记住,LLM 已经看到了各种数据,它们可以执行不同的任务,从写奇幻小说到回答复杂的推理问题。但你的目标是指导它们,如果你想让他们坚持事实,你最好在它们的角色配置文件中给出非常具体的指令。对于聊天模型,这种角色设置通常通过系统消息完成(但请记住,即使是聊天模型,所有内容也是组合成单个输入提示,在服务器端格式化)。
Gemini 提示指南建议每个提示应包含四个部分:一个角色、一个任务、一个相关上下文和一个期望的格式。请记住,不同的模型提供商可能有不同的提示编写或格式化建议,因此如果你有复杂的提示,始终检查模型提供商的文档,在切换到新的模型提供商之前评估你的工作流程的性能,并在必要时相应地调整提示。如果你想在生产中使用多个模型提供商,你可能会拥有多个提示模板,并根据模型提供商动态选择它们。
另一个重大的改进是,可以在提示中为 LLM 提供一些这个特定任务的输入输出对作为示例。这被称为少样本提示。通常,在需要长输入的场景中(例如我们将在下一章中讨论的 RAG),少样本提示难以使用,但对于相对较短的提示任务,如分类、提取等,仍然非常有用。
当然,你总是可以在提示模板本身中硬编码示例,但这会使随着系统增长而管理它们变得困难。可能更好的方式是将示例存储在磁盘上的单独文件或数据库中,并将它们加载到提示中。
将提示链在一起
随着你的提示变得更加高级,它们的大小和复杂性也会增加。一个常见的场景是部分格式化你的提示,你可以通过字符串或函数替换来实现。如果提示的某些部分依赖于动态变化的变量(例如,当前日期、用户名等),则后者是相关的。下面,你可以在提示模板中找到一个部分替换的示例:
system_template = PromptTemplate.from_template("a: {a} b: {b}")
system_template_part = system_template.partial(
a="a" # you also can provide a function here
)
print(system_template_part.invoke({"b": "b"}).text)
>> a: a b: b
另一种使你的提示更易于管理的方法是将它们分成几部分并链接在一起:
system_template_part1 = PromptTemplate.from_template("a: {a}")
system_template_part2 = PromptTemplate.from_template("b: {b}")
system_template = system_template_part1 + system_template_part2
print(system_template_part.invoke({"a": "a", "b": "b"}).text)
>> a: a b: b
你还可以通过使用langchain_core.prompts.PipelinePromptTemplate
类来构建更复杂的替换。此外,你可以将模板传递给ChatPromptTemplate
,它们将自动组合在一起:
system_prompt_template = PromptTemplate.from_template("a: {a} b: {b}")
chat_prompt_template = ChatPromptTemplate.from_messages(
[("system", system_prompt_template.template),
("human", "hi"),
("ai", "{c}")])
messages = chat_prompt_template.invoke({"a": "a", "b": "b", "c": "c"}).messages
print(len(messages))
print(messages[0].content)
>> 3
a: a b: b
动态少量提示
随着你用于少量提示的示例数量继续增加,你可能需要限制传递到特定提示模板替换中的示例数量。我们为每个输入选择示例——通过搜索与用户输入类似的内容(我们将在第四章中更多地讨论语义相似性和嵌入),通过长度限制,选择最新的等。
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_03_04.png
图 3.4:一个动态检索示例以传递给少量提示的工作流程示例
在langchain_core.example_selectors
下已经内置了一些选择器。你可以在实例化时直接将示例选择器的实例传递给FewShotPromptTemplate
实例。
思维链
2022 年初,谷歌研究团队引入了思维链(CoT)技术。他们展示了通过修改提示,鼓励模型生成逐步推理步骤,可以显著提高大型语言模型(LLM)在复杂符号推理、常识和数学任务上的性能。自那时以来,这种性能提升已被多次复制。
你可以阅读由 Jason Wei 及其同事发表的介绍 CoT 的原始论文,Chain-of-Thought Prompting Elicits Reasoning in Large Language Models:arxiv.org/abs/2201.11903
。
CoT 提示有不同的修改版本,因为它有很长的输出,通常 CoT 提示是零样本的。你添加指令鼓励 LLM 首先思考问题,而不是立即生成代表答案的标记。CoT 的一个非常简单的例子就是在你的提示模板中添加类似“让我们一步步思考”的内容。
不同的论文中报告了各种 CoT 提示。你还可以探索 LangSmith 上可用的 CoT 模板。为了我们的学习目的,让我们使用一个带有少量示例的 CoT 提示:
from langchain import hub
math_cot_prompt = hub.pull("arietem/math_cot")
cot_chain = math_cot_prompt | llm | StrOutputParser()
print(cot_chain.invoke("Solve equation 2*x+5=15"))
>> Answer: Let's think step by step
Subtract 5 from both sides:
2x + 5 - 5 = 15 - 5
2x = 10
Divide both sides by 2:
2x / 2 = 10 / 2
x = 5
我们使用了来自 LangSmith Hub 的提示——LangChain 可以使用的私有和公共工件集合。您可以在以下链接中探索提示本身:smith.langchain.com/hub.
在实践中,您可能希望将 CoT 调用与提取步骤包装在一起,以便向用户提供简洁的答案。例如,让我们首先运行一个cot_chain
,然后将输出(请注意,我们将包含初始question
和cot_output
的字典传递给下一个步骤)传递给一个 LLM,该 LLM 将使用提示根据 CoT 推理创建最终答案:
from operator import itemgetter
parse_prompt_template = (
"Given the initial question and a full answer, "
"extract the concise answer. Do not assume anything and "
"only use a provided full answer.\n\nQUESTION:\n{question}\n"
"FULL ANSWER:\n{full_answer}\n\nCONCISE ANSWER:\n"
)
parse_prompt = PromptTemplate.from_template(
parse_prompt_template
)
final_chain = (
{"full_answer": itemgetter("question") | cot_chain,
"question": itemgetter("question"),
}
| parse_prompt
| llm
| StrOutputParser()
)
print(final_chain.invoke({"question": "Solve equation 2*x+5=15"}))
>> 5
尽管 CoT 提示似乎相对简单,但它非常强大,因为我们已经提到,它已被多次证明在许多情况下显著提高了性能。当我们讨论第五章和第六章中的代理时,我们将看到其演变和扩展。
这些天,我们可以观察到所谓的推理模型(如 o3-mini 或 gemini-flash-thinking)的 CoT 模式越来越广泛地应用。在某种程度上,这些模型确实做了完全相同的事情(但通常以更高级的方式)——它们在回答之前会思考,这不仅仅是通过改变提示,还包括准备遵循 CoT 格式的训练数据(有时是合成的)。
请注意,作为使用推理模型的替代方案,我们可以通过要求 LLM 首先生成代表推理过程的输出标记来使用 CoT 修改和附加指令:
template = ChatPromptTemplate.from_messages([
("system", """You are a problem-solving assistant that shows its reasoning process. First, walk through your thought process step by step, labeling this section as 'THINKING:'. After completing your analysis, provide your final answer labeled as 'ANSWER:'."""),
("user", "{problem}")
])
自洽性
自洽性的理念很简单:让我们提高一个 LLM 的温度,多次采样答案,然后从分布中选取最频繁的答案。这已被证明可以提高基于 LLM 的工作流程在特定任务上的性能,尤其是在分类或实体提取等输出维度较低的任务上。
让我们使用前一个示例中的链并尝试一个二次方程。即使使用 CoT 提示,第一次尝试可能给出错误的答案,但如果我们从分布中进行采样,我们更有可能得到正确的答案:
generations = []
for _ in range(20):
generations.append(final_chain.invoke({"question": "Solve equation 2*x**2-96*x+1152"}, temperature=2.0).strip())
from collections import Counter
print(Counter(generations).most_common(1)[0][0])
>> x = 24
正如您所看到的,我们首先创建了一个包含由 LLM 为相同输入生成的多个输出的列表,然后创建了一个Counter
类,使我们能够轻松地找到这个列表中最常见的元素,并将其作为最终答案。
在模型提供者之间切换
不同的提供者可能对如何构建最佳工作提示有略微不同的指导。始终检查提供者侧的文档——例如,Anthropic 强调 XML 标签在结构化您的提示中的重要性。推理模型有不同的提示指南(例如,通常,您不应使用 CoT 或 few-shot 提示与这些模型)。
最后但同样重要的是,如果您正在更改模型提供者,我们强烈建议运行评估并估计您端到端应用程序的质量。
现在我们已经学会了如何高效地组织你的提示,并使用 LangChain 的不同提示工程方法,让我们来谈谈如果提示太长而无法适应模型上下文窗口时我们能做什么。
与短上下文窗口一起工作
1 百万或 200 万个标记的上下文窗口似乎足够应对我们所能想象的大多数任务。使用多模态模型,你可以向模型提问关于一个、两个或多个 PDF、图像甚至视频的问题。为了处理多个文档(摘要或问答),你可以使用所谓的stuff方法。这种方法很简单:使用提示模板将所有输入组合成一个单一的提示。然后,将这个综合提示发送给 LLM。当组合内容适合你的模型上下文窗口时,这种方法效果很好。在下一章中,我们将讨论进一步使用外部数据来改进模型响应的方法。
请记住,通常情况下,PDF 文件会被多模态 LLM 当作图像处理。
与我们两年前使用的 4096 个输入标记的上下文窗口长度相比,当前的上下文窗口长度为 100 万或 200 万个标记,这是一个巨大的进步。但仍有几个原因需要讨论克服上下文窗口大小限制的技术:
-
并非所有模型都有长上下文窗口,尤其是开源模型或在边缘上提供的服务模型。
-
我们的知识库以及我们用 LLM 处理的任务复杂性也在扩大,因为我们可能面临即使是在当前上下文窗口下的限制。
-
较短的输入也有助于降低成本和延迟。
-
像音频或视频这样的输入越来越多,并且对输入长度(PDF 文件的总大小、视频或音频的长度等)有额外的限制。
因此,让我们仔细看看我们能做什么来处理一个比 LLM 可以处理的上下文窗口更大的上下文——摘要是一个很好的例子。处理长上下文类似于经典的 Map-Reduce(一种在 2000 年代积极发展的技术,用于以分布式和并行方式处理大型数据集的计算)。一般来说,我们有两个阶段:
-
Map:我们将传入的上下文分割成更小的部分,并以并行方式对每个部分应用相同的任务。如果需要,我们可以重复这个阶段几次。
-
Reduce:我们将先前任务的结果合并在一起。
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/genai-lnchn-2e/img/B32363_03_05.png
图 3.5:一个 Map-Reduce 摘要管道
概括长视频
让我们构建一个 LangGraph 工作流程,实现上面提到的 Map-Reduce 方法。首先,让我们定义跟踪所讨论视频、在阶段步骤中产生的中间摘要以及最终摘要的图状态:
from langgraph.constants import Send
import operator
class AgentState(TypedDict):
video_uri: str
chunks: int
interval_secs: int
summaries: Annotated[list, operator.add]
final_summary: str
class _ChunkState(TypedDict):
video_uri: str
start_offset: int
interval_secs: int
我们的状态模式现在跟踪所有输入参数(以便它们可以被各个节点访问)和中间结果,这样我们就可以在节点之间传递它们。然而,Map-Reduce 模式提出了另一个挑战:我们需要调度许多处理原始视频不同部分的相似任务以并行执行。LangGraph 提供了一个特殊的 Send
节点,它允许在具有特定状态的节点上动态调度执行。对于这种方法,我们需要一个额外的状态模式,称为 _ChunkState
,来表示映射步骤。值得一提的是,顺序是有保证的——结果以与节点调度完全相同的顺序收集(换句话说,应用于主状态)。
让我们定义两个节点:
-
summarize_video_chunk
用于映射阶段 -
_generate_final_summary
用于归约阶段
第一个节点在主状态之外操作状态,但其输出被添加到主状态中。我们运行此节点多次,并将输出组合到主图中的列表中。为了调度这些映射任务,我们将创建一个基于 _map_summaries
函数的边缘,将 START
和 _summarize_video_chunk
节点连接起来:
human_part = {"type": "text", "text": "Provide a summary of the video."}
async def _summarize_video_chunk(state: _ChunkState):
start_offset = state["start_offset"]
interval_secs = state["interval_secs"]
video_part = {
"type": "media", "file_uri": state["video_uri"], "mime_type": "video/mp4",
"video_metadata": {
"start_offset": {"seconds": start_offset*interval_secs},
"end_offset": {"seconds": (start_offset+1)*interval_secs}}
}
response = await llm.ainvoke(
[HumanMessage(content=[human_part, video_part])])
return {"summaries": [response.content]}
async def _generate_final_summary(state: AgentState):
summary = _merge_summaries(
summaries=state["summaries"], interval_secs=state["interval_secs"])
final_summary = await (reduce_prompt | llm | StrOutputParser()).ainvoke({"summaries": summary})
return {"final_summary": final_summary}
def _map_summaries(state: AgentState):
chunks = state["chunks"]
payloads = [
{
"video_uri": state["video_uri"],
"interval_secs": state["interval_secs"],
"start_offset": i
} for i in range(state["chunks"])
]
return [Send("summarize_video_chunk", payload) for payload in payloads]
现在,让我们将所有这些放在一起并运行我们的图。我们可以以简单的方式将所有参数传递给管道:
graph = StateGraph(AgentState)
graph.add_node("summarize_video_chunk", _summarize_video_chunk)
graph.add_node("generate_final_summary", _generate_final_summary)
graph.add_conditional_edges(START, _map_summaries, ["summarize_video_chunk"])
graph.add_edge("summarize_video_chunk", "generate_final_summary")
graph.add_edge("generate_final_summary", END)
app = graph.compile()
result = await app.ainvoke(
{"video_uri": video_uri, "chunks": 5, "interval_secs": 600},
{"max_concurrency": 3}
)["final_summary"]
现在,随着我们准备使用 LangGraph 构建我们的第一个工作流程,还有一个最后的重要主题需要讨论。如果您的对话历史变得过长,无法适应上下文窗口,或者它可能会分散 LLM 对最后输入的注意力怎么办?让我们讨论 LangChain 提供的各种内存机制。
理解内存机制
LangChain 链和您用它们包装的任何代码都是无状态的。当您将 LangChain 应用程序部署到生产环境中时,它们也应该保持无状态,以允许水平扩展(更多关于这一点在 第九章)。在本节中,我们将讨论如何组织内存以跟踪您的生成式 AI 应用程序与特定用户之间的交互。
剪切聊天历史
每个聊天应用程序都应该保留对话历史。在原型应用程序中,您可以在变量中存储它,但这对于生产应用程序是不适用的,我们将在下一节中解决这个问题。
聊天历史本质上是一系列消息,但在某些情况下,剪切这个历史变得有必要。虽然当 LLMs 有一个有限的范围窗口时,这是一个非常重要的设计模式,但如今,它并不那么相关,因为大多数模型(即使是小型开源模型)现在支持 8192 个标记甚至更多。尽管如此,了解剪切技术对于特定用例仍然很有价值。
剪切聊天历史有五种方法:
-
根据长度丢弃消息(如标记或消息计数):你只保留最新的消息,以确保它们的总长度短于一个阈值。特殊的 LangChain 函数
from langchain_core.messages import trim_messages
允许你裁剪一系列消息。你可以提供一个函数或 LLM 实例作为token_counter
参数传递给此函数(并且相应的 LLM 集成应支持get_token_ids
方法;否则,可能会使用默认的分词器,结果可能与特定 LLM 提供商的标记计数不同)。此函数还允许你自定义如何裁剪消息 – 例如,是否保留系统消息,以及是否应该始终将人类消息放在第一位,因为许多模型提供商要求聊天始终以人类消息(或系统消息)开始。在这种情况下,你应该将原始的human, ai, human, ai
序列裁剪为human, ai
,而不是ai, human, ai
,即使所有三条消息都适合上下文窗口的阈值。 -
摘要之前的对话:在每一轮中,你可以将之前的对话摘要为一条单一的消息,并将其前置到下一个用户的输入之前。LangChain 提供了一些用于运行时内存实现的构建块,但截至 2025 年 3 月,推荐的方式是使用 LangGraph 构建自己的摘要节点。你可以在 LangChain 文档部分的详细指南中找到:
langchain-ai.github.io/langgraph/how-tos/memory/add-summary-conversation-history/
)。
在实现摘要或裁剪时,考虑是否应该在数据库中保留两个历史记录以供进一步调试、分析等。你可能希望保留最新的摘要的短期记忆历史以及该摘要之后的消息,以供应用程序本身使用,并且你可能希望保留整个历史记录(所有原始消息和所有摘要)以供进一步分析。如果是这样,请仔细设计你的应用程序。例如,你可能不需要加载所有原始历史和摘要消息;只需将新消息倒入数据库,同时跟踪原始历史即可。
-
结合裁剪和摘要:而不仅仅是简单地丢弃使上下文窗口太长的旧消息,你可以对这些消息进行摘要,并将剩余的历史记录前置。
-
将长消息摘要为短消息:你也可以对长消息进行摘要。这可能在下一章将要讨论的 RAG 用例中特别相关,当你的模型输入可能包括很多附加上下文,这些上下文是建立在实际用户输入之上的。
-
实现自己的裁剪逻辑:推荐的方式是实现自己的分词器,并将其传递给
trim_messages
函数,因为你可以重用该函数已经考虑到的很多逻辑。
当然,关于如何持久化聊天历史的问题仍然存在。让我们接下来探讨这个问题。
将历史记录保存到数据库
如上所述,部署到生产环境的应用程序不能在本地内存中存储聊天历史。如果你在多台机器上运行代码,无法保证同一用户的请求在下次轮次会击中相同的服务器。当然,你可以在前端存储历史记录并在每次来回发送,但这也会使会话不可共享,增加请求大小等。
不同的数据库提供商可能提供继承自langchain_core.chat_history.BaseChatMessageHistory
的实现,这允许你通过session_id
存储和检索聊天历史。如果你在原型设计时将历史记录保存到本地变量,我们建议使用InMemoryChatMessageHistory
而不是列表,以便以后能够切换到与数据库的集成。
让我们来看一个例子。我们创建了一个带有回调的假聊天模型,每次调用时都会打印出输入消息的数量。然后我们初始化一个保持历史记录的字典,并创建一个单独的函数,根据session_id
返回一个历史记录:
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.language_models import FakeListChatModel
from langchain.callbacks.base import BaseCallbackHandler
class PrintOutputCallback(BaseCallbackHandler):
def on_chat_model_start(self, serialized, messages, **kwargs):
print(f"Amount of input messages: {len(messages)}")
sessions = {}
handler = PrintOutputCallback()
llm = FakeListChatModel(responses=["ai1", "ai2", "ai3"])
def get_session_history(session_id: str):
if session_id not in sessions:
sessions[session_id] = InMemoryChatMessageHistory()
return sessions[session_id]
现在我们创建了一个使用len
函数和阈值1
的裁剪器——即它总是移除整个历史记录,只保留一个系统消息:
trimmer = trim_messages(
max_tokens=1,
strategy="last",
token_counter=len,
include_system=True,
start_on="human",
)
raw_chain = trimmer | llm
chain = RunnableWithMessageHistory(raw_chain, get_session_history)
现在我们运行它并确保我们的历史记录保留了与用户的全部交互,但裁剪后的历史记录被传递给了 LLM:
config = {"callbacks": [PrintOutputCallback()], "configurable": {"session_id": "1"}}
_ = chain.invoke(
[HumanMessage("Hi!")],
config=config,
)
print(f"History length: {len(sessions['1'].messages)}")
_ = chain.invoke(
[HumanMessage("How are you?")],
config=config,
)
print(f"History length: {len(sessions['1'].messages)}")
>> Amount of input messages: 1
History length: 2
Amount of input messages: 1
History length: 4
我们使用了一个RunnableWithMessageHistory
,它接受一个链并使用装饰器模式将其包装(在执行链之前调用历史记录以检索并传递给链,以及在完成链之后添加新消息到历史记录)。
数据库提供商可能将他们的集成作为langchain_commuity
包的一部分或外部提供——例如,在langchain_postgres
库中为独立的 PostgreSQL 数据库或langchain-google-cloud-sql-pg
库中为托管数据库。
你可以在文档页面上找到存储聊天历史的完整集成列表:python.langchain.com/api_reference/community/chat_message_histories.html。
在设计真实的应用程序时,你应该小心管理对某人会话的访问。例如,如果你使用顺序的session_id
,用户可能会轻易访问不属于他们的会话。实际上,可能只需要使用一个uuid
(一个唯一生成的长标识符)而不是顺序的session_id
,或者根据你的安全要求,在运行时添加其他权限验证。
LangGraph 检查点
检查点是对图当前状态的快照。它保存了所有信息,以便从快照被捕获的那一刻开始继续运行工作流程——包括完整的状态、元数据、计划执行的任务节点以及失败的任务。这与存储聊天历史记录的机制不同,因为你可以在任何给定的时间点存储工作流程,稍后从检查点恢复以继续。这有多个重要原因:
-
检查点允许深入调试和“时间旅行”。
-
检查点允许你在复杂的流程中尝试不同的路径,而无需每次都重新运行它。
-
检查点通过在特定点实现人工干预并继续进一步,促进了人工介入的工作流程。
-
检查点有助于实现生产就绪的系统,因为它们增加了所需的持久性和容错级别。
让我们构建一个简单的示例,其中包含一个打印状态中消息数量并返回假AIMessage
的单节点。我们使用一个内置的MessageGraph
,它表示一个只有消息列表的状态,并初始化一个MemorySaver
,它将在本地内存中保存检查点,并在编译期间将其传递给图:
from langgraph.graph import MessageGraph
from langgraph.checkpoint.memory import MemorySaver
def test_node(state):
# ignore the last message since it's an input one
print(f"History length = {len(state[:-1])}")
return [AIMessage(content="Hello!")]
builder = MessageGraph()
builder.add_node("test_node", test_node)
builder.add_edge(START, "test_node")
builder.add_edge("test_node", END)
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
现在,每次我们调用图时,我们都应该提供一个特定的检查点或线程-id(每次运行的唯一标识符)。我们用不同的thread-id
值调用我们的图两次,确保它们每个都以空的历史记录开始,然后检查当我们第二次调用它时,第一个线程有一个历史记录:
_ = graph.invoke([HumanMessage(content="test")],
config={"configurable": {"thread_id": "thread-a"}})
_ = graph.invoke([HumanMessage(content="test")]
config={"configurable": {"thread_id": "thread-b"}})
_ = graph.invoke([HumanMessage(content="test")]
config={"configurable": {"thread_id": "thread-a"}})
>> History length = 0
History length = 0
History length = 2
我们可以检查特定线程的检查点:
checkpoints = list(memory.list(config={"configurable": {"thread_id": "thread-a"}}))
for check_point in checkpoints:
print(check_point.config["configurable"]["checkpoint_id"])
让我们再从thread-a
的初始检查点恢复。我们会看到我们从一个空的历史记录开始:
checkpoint_id = checkpoints[-1].config["configurable"]["checkpoint_id"]
_ = graph.invoke(
[HumanMessage(content="test")],
config={"configurable": {"thread_id": "thread-a", "checkpoint_id": checkpoint_id}})
>> History length = 0
我们也可以从一个中间检查点开始,如下所示:
checkpoint_id = checkpoints[-3].config["configurable"]["checkpoint_id"]
_ = graph.invoke(
[HumanMessage(content="test")],
config={"configurable": {"thread_id": "thread-a", "checkpoint_id": checkpoint_id}})
>> History length = 2
检查点的一个明显用途是实现需要用户额外输入的工作流程。我们将遇到与上面完全相同的问题——当我们将我们的生产部署到多个实例时,我们无法保证用户的下一个请求击中与之前相同的服务器。我们的图是状态性的(在执行期间),但将其作为网络服务封装的应用程序应该保持无状态。因此,我们无法在本地内存中存储检查点,而应该将它们写入数据库。LangGraph 提供了两个集成:SqliteSaver
和PostgresSaver
。你可以始终将它们作为起点,并在需要使用其他数据库提供者时构建自己的集成,因为你需要实现的是存储和检索表示检查点的字典。
现在,你已经学到了基础知识,并且已经完全准备好开发你自己的工作流程。我们将在下一章继续探讨更复杂的一些示例和技术。
摘要
在本章中,我们深入探讨了使用 LangChain 和 LangGraph 构建复杂工作流程,超越了简单的文本生成。我们介绍了 LangGraph 作为一种编排框架,旨在处理代理工作流程,并创建了一个基本的工作流程,包括节点和边,以及条件边,允许工作流程根据当前状态进行分支。接下来,我们转向输出解析和错误处理,展示了如何使用内置的 LangChain 输出解析器,并强调了优雅错误处理的重要性。
然后,我们探讨了提示工程,讨论了如何使用 LangChain 的零样本和动态少样本提示,如何构建高级提示,如 CoT 提示,以及如何使用替换机制。最后,我们讨论了如何处理长和短上下文,探索了通过将输入拆分为更小的部分并按 Map-Reduce 方式组合输出来管理大上下文的技术,并处理了一个处理大型视频的示例,该视频不适合上下文。
最后,我们涵盖了 LangChain 中的内存机制,强调了在生产部署中保持无状态的需求,并讨论了管理聊天历史的方法,包括基于长度的修剪和总结对话。
我们将在这里学到的知识用于在 第四章 中开发 RAG 系统,以及在 第五章 和 第六章 中开发更复杂的代理工作流程。
问题
-
LangGraph 是什么,LangGraph 工作流程与 LangChain 的标准链有什么不同?
-
LangGraph 中的“状态”是什么,它的主要功能是什么?
-
解释 LangGraph 中 add_node 和 add_edge 的作用。
-
LangGraph 中的“supersteps”是什么,它们与并行执行有什么关系?
-
与顺序链相比,条件边如何增强 LangGraph 工作流程?
-
在定义条件边时,Literal 类型提示的作用是什么?
-
LangGraph 中的“reducers”是什么,它们如何允许修改状态?
-
为什么错误处理在 LangChain 工作流程中至关重要,以及实现它的策略有哪些?
-
如何使用内存机制来修剪对话机器人的历史记录?
-
LangGraph 检查点的用例是什么?
订阅我们的每周通讯
订阅 AI_Distilled,这是人工智能专业人士、研究人员和创新者的首选通讯,请访问 packt.link/Q5UyU
。
更多推荐
所有评论(0)