LangChain 在生命科学与医疗保健领域的应用——LangChain在化学领域的应用

115 阅读57分钟

本章开篇别无他法。2024年诺贝尔化学奖一半授予了大卫·贝克(David Baker),表彰其在计算蛋白质设计领域的贡献,另一半则由德米斯·哈萨比斯(Demis Hassabis)和约翰·贾姆珀(John Jumper)共同获得,以表彰他们在蛋白质结构预测方面的突破。DeepMind团队与贝克实验室开发的工具利用人工智能解决了两个长期吸引科学家关注的相关难题:仅凭蛋白质的氨基酸序列如何确定其三维结构,以及如何设计出具有自然界前所未见的形状和功能的新蛋白质。

微软、英伟达、IBM等科技公司利用人工智能提升我们对化学的理解。他们运用生成式AI,加快新药物、新材料和新产品的发现速度。相比传统的体外(in vitro)或体内(in vivo)测试,这种技术使得先在计算机中(in silico)进行新想法的验证更加高效,从而节省时间和资金。

大量生成式AI的努力集中在药物发现领域(第8章有详细讨论),但还有许多其他应用示例。微软利用Azure Quantum Elements推动材料科学研究,这一工具预计将结合生成式AI与量子计算,更高效地发现和开发具有特定性能的新材料。联合利华等公司借助此类技术更快地创造出更优质的产品,如个性化的护发方案,同时减少浪费。微软还与约翰逊·马修(Johnson Matthey)等公司合作,致力于解决气候变化问题。

SAP和巴斯夫(BASF)运用AI提高化工生产效率。专注于新材料开发的Albert Invent公司筹集了2000万美元以扩大其AI工具应用。专注制造业AI的Basetwo获得1150万美元投资,帮助化工企业提升生产力。聚焦化学领域AI的初创公司Rowan Scientific也获得了210万美元资金支持。西门子斥资51亿美元收购Dotmatics,提升科学研究软件;被Alphabet旗下SandboxAQ收购的Good Chemistry正在研发化学研究领域的AI与量子计算技术。类似的合作和投资层出不穷。如今,生命科学公司前所未有地与科技公司携手,拓展影响力并提升AI系统质量。

生成式AI在化学领域

正如第1章所述,生成式AI模型可产生多种类型的输出。在化学领域,最重要的三类生成式AI分别是:基于文本的、基于代码的和基于化学的。这一分类主要依据输出类型或核心组成,当然也存在交叉边界情况。本节将探讨这些应用的可能方向。

文本类

文本类应用主要侧重于文本处理和生成输出文本。大多数此类应用以语言和书面文本为信息来源,致力于理解和生成自然语言,帮助科学家提高工作效率。它们能够处理成千上万的研究论文、实验室笔记和化学数据库(例如带有描述字段的数据库),从中发现规律和联系。

基于化学文献训练的语言大模型(LLM)可比传统搜索更快地提供反应、机制或材料性能等具体细节,加速文献综述。这种速度优势源于模型能同时处理和综合数千篇论文的信息,而不必研究者逐篇阅读。通过压缩大量化学研究论文并突出关键发现与方法,LLM帮助研究人员及时掌握最新进展。

现代LLM能从论文中提取实验条件、产率和表征数据,组织成表格或结构化格式,便于多项研究间的对比。它们可识别同一主题文献中的矛盾或不一致,指出需要进一步研究或澄清的领域。在总结一组同主题论文时,这些模型还能追踪方法和认知的演变过程,展现某一领域知识的进步。它们还能基于未解答的问题识别当前研究空白。

LLM理解上下文和化学概念,即使查询中未出现精确搜索词,也能识别相关信息。此外,LLM还能根据研究者的历史查询和兴趣个性化搜索结果,逐步构建更有帮助的检索体验。它们还可根据信息来源的可信度、研究的时效性或实验验证状态过滤结果,帮助研究者优先关注最可靠的信息。

经过微调的LLM能解释化学概念,比如为何会出现特定的氧化还原电位,尽管这些解释有时需更加准确或完整。训练有素的模型能将复杂化学过程拆解为更简单的组成部分,生成假设,并通过逐步建议路径和解释推理,规划实验以优化合成路线。

LangChain应用可检索科研设备配置和潜在自动化相关信息。未来的应用,包括第5章讨论的模型上下文协议(MCP),将能通过文本命令管理实验室设备,展示其通过基于文本的界面实现实验最优配置的能力。这些应用还能实时执行操作和监控设备状态,及时提醒研究者潜在问题,防止实验失败。同时,它们可维护并总结实验过程中的详细设备参数日志,确保实验的可重复性和问题排查能力。

代码生成类

在基于代码的应用中,核心功能是代码生成。输出可能是数字、公式或图像,但它们都是通过函数执行、工具调用或API请求生成的结果。这类应用将研究者用自然语言描述的需求与实现这些需求所需的技术代码结合起来。通常,这些应用还内置验证机制,确保生成的代码符合化学原理和物理定律。

提示
想象你想要可视化一个分子或绘制图表。虽然视觉语言模型的能力不断提升,但正如序言和第4章所示,生成的图形更多带有艺术性质,而非纯科学。在本章及后续章节中,我们会深入探讨这一概念。

代码生成应用可以分为两类:如果你懂编程,它们能辅助你完成任务;如果不懂,它们可能直接生成最终产品。第5章展示了我们设计的可以自我生成并执行代码的智能体(本章也会讨论此类智能体)。对于使用代码助手的程序员,这些应用还提供额外好处,比如生成文档、提供代码优化建议,从而加快计算速度或提升效率。这一点在处理大规模分子数据集或复杂模拟时尤为重要。

LangChain工具可以用来调用如PubMed或ChemSpider等API检索化学信息,或执行预定义方法,而LLM则能利用Python包动态生成代码。生成的代码可用于计算、数据分析和性能预测,极大地推动化学研究的变革。在本章后续部分,你将学会如何利用基于LLM的RDKit或其他软件包执行多项任务。这些工具可自动格式化输入数据,满足API的要求,使研究者无需掌握每个API的具体语法,就能轻松访问外部数据库。它们还能够串联多个API调用和数据处理步骤,构建原本需要大量手写代码的工作流。随着计算化学软件的发展,对于接触新或陌生化学软件包的研究者,这些工具还可生成示例代码,展示如何完成特定任务,既是解决方案,也是学习资源。同时它们会跟进最新功能和语法,助力研究者充分利用新能力。

通过设计LLM提示词,我们可以为Gaussian、GAMESS等计算化学程序生成输入文件,指定分子结构、计算方法、期望输出及其他参数。这极大简化了过去繁琐且易出错的手动过程。工具还能在提交作业前检查输入规范中的常见错误或不一致,节省因设置错误而浪费的计算资源。它们还支持生成多组输入变体,用于敏感性分析,帮助研究者测试不同计算参数对结果的影响。

化学生成类

在基于化学的应用中,核心组件主要是针对化学领域微调的LLM。应用可能对编码器有不同的输入要求,但解码器的输出通常是图结构、简化分子输入线性表达式(SMILES)或其他化学或材料相关的形式。这些应用代表了新化学实体发现方式的根本转变,从传统的试错或渐进式修改转向定向的计算设计。它们能够同时纳入多个领域的约束条件,如合成可行性、生物活性和物理性质,从而生成满足复杂需求集合的分子。

化学调优LLM的主要用途之一是生成分子结构。基于文本描述、前体物、期望的化学特性或其他正确输入编码器的数据,模型可以创造潜在的分子结构。第8章介绍了一个分子生成的示例。这个过程将加速新材料和试剂的筛选。例如,可以开发应用来探索嵌段聚合物,并根据化学约束设计分子。另一种可能的功能是根据文本输入调整化学数据,比如用LangChain应用改良现有分子的性质,如溶解度。

这些系统能够围绕一个有前景的先导结构生成完整的化合物集合,创造出在现实中合成不切实际的化学空间。它们在生成分子时会考虑合成路径,确保最终分子能够在实验室中以合理的努力和资源被合成,比如生成可从公司现有化学库存或专利中合成的化合物。

另一个化学生成应用领域是预测化学反应。利用调优过的LLM,LangChain应用可能预测反应的产物和中间体,帮助化学家设计高效且选择性强的合成路线,提前预判潜在问题。这也可由LLM单独完成,但LangChain应用通过外部工具和智能体,能更好地提取信息和优化结果。这些预测系统可根据可能性对反应路径进行排序,帮助化学家优先关注最有前景的路线,还能标记可能降低产率或增加纯化难度的副反应或竞争反应。

类似之前的示例,LangChain应用还能基于文献报道的相似转化生成催化剂,或通过预测条件变化对催化活性的影响,建议最优反应条件。对于复杂的多步合成,它们能绘制完整的反应树,识别从可用起始物到目标化合物的最高效路径。还能提出替代反应路线,提供考虑实际约束的实用方案。在反应优化方面,这些系统可以建议有效探索参数空间的实验方案,减少寻找最佳条件所需的实验次数。对于绿色化学应用,它们能特别针对减少废物、降低能耗或消除有害试剂的条件,同时保持反应效率。

当然,这里并未涵盖所有潜在用例,许多实际化学应用超出此分类,或涵盖多类别。真实世界中,许多化学应用需综合运用这些方法,整合文本理解、代码生成和化学专用输出,解决复杂问题。一个简单且常用的例子是利用文本和化学生成应用实现化合物名称与SMILES表达式的转换。代码生成应用还能调用专门API和代码包输出SMILES。这些转换应用支持多种命名规范,从IUPAC名称到常用名、商品名,甚至实验室使用的非正式或缩写名称。

一个高级混合应用可能使用基于文本的组件从专利和论文中收集信息,基于代码的组件执行模拟和数据分析,基于化学生成的组件设计候选分子。这些集成系统通常提供端到端解决方案,涵盖从初始概念到最终输出的完整流程。随着领域进展,这些类别间的界限会逐渐模糊,应用旨在为化学研究挑战提供更全面的解决方案。

下一节将探讨纯文本和代码模型如何用于开发应用。随后,我们将探索LangChain如何应用于化学文本清理,以及化学生成工具如何利用化学模型。

使用外部包创建应用程序

本节将演示如何利用LangChain增强化学研究和分析,并改进其他常见任务。作为示例,我选择了一些简单的基准测试,比如分子式的确定、分子量计算,以及从SMILES表示法绘制化学结构的二维和三维可视化。我们将探讨不同的策略、各方法的具体细节,以及它们如何有助于实现化学中的准确且高效的结果。

为了评估这些应用的质量,我们将围绕阿司匹林的SMILES表示式CC(=O)OC1=CC=CC=C1C(=O)O提出几个问题:
问题1:CC(=O)OC1=CC=CC=C1C(=O)O的分子式是什么?
问题2:CC(=O)OC1=CC=CC=C1C(=O)O的分子量是多少?
问题3:绘制CC(=O)OC1=CC=CC=C1C(=O)O的二维结构图。
问题4:绘制CC(=O)OC1=CC=CC=C1C(=O)O的三维结构图。

这些问题(除问题4外)的答案来源于PubChem资源,如图6-1所示。

image.png

首先,我们将了解ChemCrow,这是一个强大的工具,利用大型语言模型的能力来理解和生成化学信息。它拥有令人兴奋的功能,比如我们本次使用的SMILES2Name和SMILES2Weight,以及集成了RDKit、rxn4chemistry、PubChem和ChemSpace等资源。ChemCrow的第一个版本于2023年4月发布,次年便在《自然》杂志上发表。这一方法展示了生成式AI在高精度理解和处理化学数据方面的实用价值。

我们将观察ChemCrow在基准测试中的表现,但在此之前,我想介绍ChemCrow集成的安全工具,以确保化学任务的负责任和安全执行:

  • ControlledChemicalCheck 会将CAS编号与管控物质列表进行交叉核查,防止合成危险化学品,一旦匹配即停止操作。
  • ExplosiveCheck 通过查询PubChem数据库验证分子的爆炸性,必要时发出警告。
  • SafetySummary 提供分子的全面安全概况,包括操作安全性、全球化学品统一分类和标签制度(GHS)信息、环境风险和社会影响,并利用LLM填补数据空白。

这些工具共同提升了ChemCrow的安全性,能预先识别潜在风险,确保符合安全标准,并为用户提供关键安全信息。

警告
在利用LLM普及化学、医学和生物知识时,安全至关重要。尽管这些工具可大幅推进科研与教育,开发者必须预见可能的滥用及其严重后果。化学合成的错误指导可能导致危险物质或不安全的实验条件,威胁研究人员和环境安全。此外,这些工具被滥用可能助长非法药物或化学武器的制造。开发者必须实施严格的安全检查、验证协议和伦理指南,确保LLM的强大能力被负责任且安全地使用。

以ChemCrow作为稳固的基准,我们将考察纯gpt-3.5-turbo、gpt-4o-mini和o3-mini模型在类似任务中的表现。通过比较这些模型的输出,我们将深入了解独立生成式AI模型在化学领域应用的优势与局限。该比较将突出不同版本GPT在化学数据解析上的性能和准确性的差异。

我们还将探讨LangChain简单链的实现,它结合了提示技术和OpenAI模型。这种方法通过创建一个简单的提示,指示LLM它是一位世界级的化学专家。通过检验这种链式方法的有效性,我们将了解简单提示如何帮助实现化学分析中的期望结果。

此外,我们将开发一个配备了专用工具(如ReturnFormula、ReturnMolecularWeight、Return2DMolecule和Return3DMolecule)的自定义ReAct代理。我们会展示如何设计工具并指导LangChain代理执行所需任务。

最后,我们将介绍一种抓取所有代码文档并使用FAISS检索器构建代理的方法,使其能在清理和拆分后的RDKit文档中进行搜索。我们将探讨这种方法如何让LLM提供更准确且具有上下文相关性的答案,展示结合生成式AI与丰富文档资源的潜力。

注意
生成式AI的实验与传统软件测试有很大不同,主要由于其固有的非确定性。传统软件对给定输入产生一致输出,而基于LLM的生成式AI系统同一输入在相同温度参数下可能产生差异较大的输出。这种变异源于算法的概率性质,从可能的响应分布中采样,加入随机性以增强创造力和多样性,详见第2章。

我会附上代码片段,演示不同模型、链和代理的工作方式。完整代码可在LangChain4Life Sciences Healthcare仓库中找到。

ChemCrow 与 CACTUS

目前,ChemCrow 的最新版本(0.3.24)尚不支持 LangChain 0.1+,这使得它与其他代理协同使用变得具有挑战性。我们将使用两个 OpenAI 模型运行 ChemCrow 代理:gpt-3.5-turbo 和 gpt-4o-mini。这些模型承担多重任务:一个模型负责与用户对话和改写,另一个模型负责调用工具,正如以下所示。尽管我们感兴趣的工具并不需要语言模型,但在研究的两种情况下,我们都为它们设置了相同的模型:

from chemcrow.agents import ChemCrow
chem_model = ChemCrow(model=..., tools_model=..., temp=0)

示例 6-1 中,对创建的 ChemCrow 实例同时应用了两个模型,将阿司匹林的 SMILES 转换为分子式。值得注意的是,ChemCrow 并没有内置此功能的工具。正如示例 6-1 所示,gpt-3.5-turbo 模型驱动的代理在返回正确答案 C9H8O4 之前调用了两个无关工具(Name2SMILES 和 SMILES2Weight)。而 gpt-4o-mini 模型则返回了明显错误的 C10H10O4。

示例 6-1. ChemCrow 对第一个查询的回答

query_1 = "What is the formula of CC(=O)OC1=CC=CC=C1C(=O)O?"
chem_model.run(query_1)
  • gpt-3.5-turbo 输出正确答案 C9H8O4,调用了 Name2SMILES 和 SMILES2Weight 两个工具。
  • gpt-4o-mini 直接返回错误答案 C10H10O4。

示例 6-2 显示了第二个查询的结果。为了确定分子质量,gpt-3.5-turbo 的 ChemCrow 实例选择了 SMILES2Weight 工具并返回正确结果。gpt-4o-mini 模型也返回了正确答案,但未使用任何工具。

示例 6-2. ChemCrow 对第二个查询的回答

query_2 = "What is the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O?"
chem_model.run(query_2)
  • gpt-3.5-turbo 使用 SMILES2Weight 工具,返回约 180.042 g/mol。
  • gpt-4o-mini 直接返回 180.16 g/mol。

接下来是第三和第四个查询,涉及绘制阿司匹林的二维和三维结构图。关于二维表示(见示例 6-3),两个模型都意识到正在处理分子并需要检查其是否为管控物质。gpt-3.5-turbo 调用了 ControlChemCheck 工具以确定是否可以继续,而 gpt-4o-mini 直接返回拒绝协助,提示需进一步检查管控或爆炸性状态。

绘制三维表示时,gpt-3.5-turbo 驱动的 ChemCrow 实例调用了 Python_REPL 工具执行模型生成的代码,但结果错误。这体现了语言模型的不确定性和工具选择时的“幻觉”问题。

示例 6-3. ChemCrow 中错误的工具定义

query_3 = "Draw 2D representation of CC(=O)OC1=CC=CC=C1C(=O)O."
chem_model.run(query_3)
  • gpt-3.5-turbo 检查后确认分子不是管控物质,调用 Name2SMILES,最终给出带苯环和两个邻接羰基的结构描述。
  • gpt-4o-mini 拒绝协助,要求用户提供不同问题或更详细说明。

总结来看,ChemCrow 代理在部分化学任务中表现有用,部分查询通过内置工具和 Python_REPL 解决。除调用了无关工具外,还有其它问题需解决,比如包的初始设置和导入。目前用户必须声明所有API密钥(包括未使用的),且对本地LLM支持有限,化学功能受限,定制选项不足。例如,禁用详细模式需要明确声明OpenAI密钥,体验不佳。此外,最大的缺陷是与最新 LangChain 更新不兼容。

与 ChemCrow 类似,CACTUS(Chemistry Agent Connecting Tool-Usage to Science)库也为代理设置了几种工具,但同样受限于硬编码的 LLM 和工具。唯一成功完成的任务是分子质量计算。完整结果见 LangChain4LifeSciencesHealthcare 仓库。

这些情况表明 ChemCrow 适合初学者使用,但对于寻求广泛定制和更丰富功能的高级用户可能只能部分满足需求。这个实验也反映出代理依赖自带工具时的局限性,后续章节我们将深入探讨如何构建自定义代理以突破这些限制。

LLMs

现在,既然我们已经设定了基准,让我们来探索原始模型的能力。gpt-4o-mini 模型在多个基准测试中表现出比 gpt-3.5-turbo 模型更优越的准确性。例如,gpt-4o-mini 在学术考试和专业测试中得分很高,反映出其先进的理解和解决问题的能力。这使它在需要详细且精确答案的任务中,尤其是在科学和技术领域,显得尤为有价值。

你可能会问,为什么没有将领域专用模型与 GPT 模型进行比较。我确实做了这个比较,结果并不理想(ChemLLM 在四个问题中只答对一个,LLM4Chem 的 LlaSMol-Llama2-7B 模型则是四个问题全错),如图 6-2 所示,这里展示的是 AI4Chem/ChemLLM-20B-Chat-DPO 模型的表现。

image.png

模型可能经过针对特定基准的调优,但即使我们用 <SMILES> 标签包裹 SMILES,也没有起到很好效果。此外,虽然我们期望另一个查询(示例 6-4)能正确回答分子式,但生成的分子式仍然错误(显示为 C11H10O5,而非正确的 C9H8O4)。

示例 6-4. LLM4Chem 回答四个查询

from generation import LlaSMolGeneration
generator = LlaSMolGeneration('osunlp/LlaSMol-Llama2-7B')

> generator.generate("What is the formula of CC(=O)OC1=CC=CC=C1C(=O)O?")
>>> <productIUP> CCCCCC...CCCCCCCCCCCCMS(CCCCCCCCCCCCMSMSMS...MSMSMSMS

> generator.generate("What is the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O?")
>>> <loginst> P2 </loginst> What is the molecular formula of CC(=O)OC1=CC=CC=C1C(=O)O? <loginst> CCCCCCCC...CCCCCCCCS </loginst> </loginst> </loginst </loginst>
...

> generator.generate("Draw 2D representation of CC(=O)OC1=CC=CC=C1C(=O)O.")
>>> <SMILES> CC(=O)OC1=CC=CC=C1C(=O)O </SMILES> </SMILES> The molecule is a benzoate ester obtained by formal condensation of the carboxy group of benzoic acid with the hydroxy group of 2-acetylphenol. It is a benzoate ester and a member of benzoic acids ....

> generator.generate("What is the formula of <SMILES> CC(=O)OC1=CC=CC=C1C(=O)O </SMILES>?")
>>> <SMILES> CC(=O)OC1=CC=CC=C1C(=O)OCC1=CC=CC=C1 </SMILES> .</s>

> generator.generate("What is the molecular mass of <SMILES> CC(=O)OC1=CC=CC=C1C(=O)O </SMILES>?")
>>> <MOLFORMULA> C11H10O5 </MOLFORMULA> .</s>

正如本章和第二章讨论的,确实需要领域专用的 LLM,但主要用于专家级回答。LLM 不是计算器,有时会出现算术错误,因此不能完全信任所有计算结果。大多数化学模型,如本章和第二章提到的 LlaSMol、ChemLLM 等,都是为解决特定任务而训练的(有时仅仅是为了超越基准测试)。尽管经过领域数据的微调,这些模型通常较小,而一些大型模型可能也使用相同的领域数据训练。此外,随着新化合物的创造和新数据的收集,这些模型无法及时跟进(第三章有讨论)。我们将比较纯模型、带提示的版本和工具驱动的代理,以判断通用模型是否能够决定使用哪个工具来解决领域问题。

模型初始化将使用 LangChain 的 ChatOpenAI 包装器,温度设置为 0。我们会按照相同顺序重复相同查询,先从阿司匹林的经验式分子式开始。gpt-3.5-turbo 模型回答错误,给出 C12H10O4。令人惊讶的是,gpt-4o-mini 没有失败(与之前的 ChemCrow 示例不同),返回了正确的回答:“This compound is known as acetylsalicylic acid, commonly known as aspirin”,并给出了正确的分子式。虽然 gpt-3.5-turbo 回答正确,但 gpt-4o-mini 展现了一些正确的推理能力,比如计算 {Molecular mass} = (10 × 12.01) + (10 × 1.008) + (4 × 16.00),不过它应用的是错误的分子式(模型有此能力,前面已经展示过),导致最终结果错误。

注意:正确识别阿司匹林的经验式分子式和分子质量,可能得益于训练数据中包含了来自维基百科和其他百科全书的文章。

两个模型都无法绘制 2D 或 3D 分子结构,但都尝试解释如何绘制,说明两个模型理解了“绘制”这个动词的含义,就像在纸上绘画一样。总体来说,两个模型表现不佳,四个查询中只有一个正确答案。

o3-mini 推理模型表现更好。它对经验式分子式和分子质量进行了计算,得出最终答案,代码片段如下:

# o3-mini 经验式分子式推理
• Carbon (C): 2 (来自 CH3CO) + 6 (苯环) + 1 (羧酸) = 9
• Hydrogen (H): 3 (来自 CH3) + 4 (苯环) + 1 (–OH) = 8
• Oxygen (O): 1 (来自 CH3CO 羰基) + 1 (连接乙酰基与环的酯氧) + 2 (羧酸) = 4

因此,总分子式是 C9H8O4。
# o3-mini 分子质量推理
• Carbon (C): 9 × 12.01 g/mol ≈ 108.09 g/mol
• Hydrogen (H): 8 × 1.008 g/mol ≈ 8.06 g/mol
• Oxygen (O): 4 × 16.00 g/mol ≈ 64.00 g/mol
总分子质量 ≈ 108.09 + 8.06 + 64.00 = 180.15 g/mol

因此,CC(=O)OC1=CC=CC=C1C(=O)O 的分子质量约为 180.15 g/mol。

最令人惊讶的是 2D 和 3D 表示。由于模型输出是文本,通常会有以下四种可能结果,按常见度排序:

  1. 无法绘制分子
  2. 描述如何绘制分子
  3. 试图以 base64(原始图像格式)显示分子
  4. 试图用文本符号(ASCII 格式)画出分子结构

可以较为确定的是,训练数据中不存在所有分子的草图格式,这意味着模型必须理解分子的二维和三维结构并进行再现。以下是结果示例:

# o3-mini 2D 结构
                 O
                 ║
           CH₃–C–O
                 │
                 │         (乙酰氧基在环的 C2 位)
        .--------┴--------.
       /         │         \
      |   (2)   [环]     (6) |
      |           │         |
  COOH–(1)        (3)       (5)
       \         │         /
        '--------┬--------'
               (4)
# o3-mini 3D 结构
         CH₃
          |
          C=O
          |
          O     ← 乙酰氧氧原子(略微抬高)
           \
            •–––[C6H4]––•   ← 苯环(• 表示环碳)
           /            \
  –C(=O)OH               (环的其余部分)
          ↑
   (羧酸羰基氧)
          |
         OH

尽管分子结构并不完全正确(两个乙酰氧基画错了),但模型的能力仍然令人印象深刻。

LCEL链

现在是学习如何通过提示(prompting)提升 gpt-3.5-turbo 和 gpt-4o-mini 模型性能的时候了。我没有包含 o3-mini 模型,因为我们使用的简单提示对它的推理能力影响不大。示例 6-5 展示了对 gpt-3.5-turbo 和 gpt-4o-mini 两个模型加上了一个提示,要求模型以世界级化学专家的身份回答,并使用链式思考(Chain-of-Thought,CoT)策略,让模型逐步思考。

两个模型的 LCEL 链都未能正确计算阿司匹林的经验式分子式,这对 gpt-4o-mini 链来说表现不如纯 LLM 测试。好的一面是,gpt-4o-mini 链正确计算出了分子质量;因此在这个示例中,经验式分子式的错误也反映了语言模型生成标记的不稳定性。

示例 6-5. LCEL 链

template = """
    You are a world-class specialist in chemistry.
    Return only the final answer. Think step by step.
    Question: {question}
    Answer: Let's think step by step.
"""
prompt = ChatPromptTemplate.from_template(template)
chain = prompt | model

# gpt-3.5-turbo
> chain.invoke(
    {"question":"What is the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O?"}
)
#-------------------------AI生成内容-------------------------
>>>
Step 1: Calculate the molecular mass of each element in the compound:\n- Carbon
        (C): 12.01 g/mol\n- Oxygen (O): 16.00 g/mol\n- Hydrogen (H): 1.01 g/mol
Step 2: Count the number of each element in the compound:\n- C: 9\n- O: 3\n- H: 8
Step 3: Calculate the total molecular mass by multiplying the number of each element
        by its atomic mass and adding them all together:\n(9 x 12.01) + (3 x 16.00)
        + (8 x 1.01) = 108.09 + 48.00 + 8.08 = 164.17 g/mol

Final Answer: The molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O is 164.17 g/mol.
#-------------------------结束-------------------------

# gpt-4o-mini
> chain.invoke(
    {"question": "What is the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O"}
)
#-------------------------AI生成内容-------------------------
>>> To calculate the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O, we will break down
the molecule into its constituent atoms and their respective counts.
1. **Identify the structure**: The molecule can be broken down as follows:\n   - CC
    (=O) indicates an acetyl group (C2H3O).\n   - OC1=CC=CC=C1 indicates a phenyl
    ring (C6H5) with an ether linkage.\n - C(=O)O indicates a carboxylic acid group
    (COOH).
2. **Count the atoms**:\n   - From CC(=O): 2 Carbon (C) and 3 Hydrogen (H) and 1
    Oxygen (O).\n   - From OC1=CC=CC=C1: 6 Carbon (C) and 5 Hydrogen (H).\n   - From
    C(=O)O: 1 Carbon (C) and 2 Oxygen (O) and 1 Hydrogen (H).
3. **Total counts**:\n   - Total Carbon (C): 2 (from acetyl) + 6 (from phenyl) + 1
    (from carboxylic) = 9 C\n   - Total Hydrogen (H): 3 (from acetyl) + 5 (from
    phenyl) + 1 (from carboxylic) = 9 H\n   - Total Oxygen (O): 1 (from acetyl) + 1
    (from ether) + 2 (from carboxylic) = 4 O
4. **Calculate the molecular mass**:\n   - Carbon (C): 12.01 g/mol × 9 = 108.09 g/
    mol\n   - Hydrogen (H): 1.008 g/mol × 9 = 9.072 g/mol\n   - Oxygen (O): 16.00 g/
    mol × 4 = 64.00 g/mol
5. **Sum the masses**:\n   - Total molecular mass = 108.09 g/mol + 9.072 g/mol + 64.
    00 g/mol = 181.162 g/mol

Final Answer: 181.16 g/mol
#-------------------------结束-------------------------

因此,可以将 LCEL 链的表现与纯 LLM 的表现进行比较。

自定义 LangChain 代理

你已经在第3章和第5章中使用过 LangChain 的代理和工具了。在这个策略中,我们将创建一个基于 gpt-4o-mini 的 ReAct 代理,并配备所有必要的工具:ReturnFormula、ReturnMolecularWeight、Return2DMolecule 和 Return3DMolecule。在示例6-6中,你可以看到定义自定义函数和自定义工具的多种方法之一,并将该工具设置为代理使用。我们会稍微修改 ReAct 的提示,避免函数无限循环调用。否则,在显示完图片后,函数返回 None,会让代理误以为工具调用失败或出错,从而可能重复调用该工具。

示例6-6. 带工具的 LangChain 自定义代理

# 定义函数
def return_formula(smiles: str) -> str:
    """返回给定 SMILES 字符串的分子式。"""
    mol = get_mol(smiles)
    if mol is None:
        raise ValueError(f"无效的 SMILES 字符串: {smiles}")
    return CalcMolFormula(mol)

# 定义工具
class ReturnFormula(BaseTool):
    name: str = "return_formula"
    description: str = "返回给定 SMILES 的经验化学式"
    def _run(self, smiles: str):
        return return_formula(smiles)

# ... 其他函数和工具定义

tools = [
    ReturnFormula(), ReturnMolecularWeight(), Return2DMolecule(), Return3DMolecule()
]

template = """请尽力回答以下问题。你可以使用以下工具:
{tools}
...
如果工具返回 True,则认为任务完成。
任务完成后,请退出链。
开始!
问题:{input}
思考过程:{agent_scratchpad}"""

prompt = PromptTemplate.from_template(template)

agent = create_react_agent(openai_handler.model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

> agent_executor.invoke(
    {"input": "What is the formula of CC(=O)OC1=CC=CC=C1C(=O)O?"}
)
#-------------------------AI生成内容-------------------------
> 进入新的 AgentExecutor 链...
为了找到给定 SMILES CC(=O)OC1=CC=CC=C1C(=O)O 的经验化学式,
我将使用合适的工具来返回分子式。
动作:return_formula
动作输入:CC(=O)OC1=CC=CC=C1C(=O)O
C9H8O4
我已经知道最终答案了
最终答案:C9H8O4
#-------------------------结束-------------------------

所有工具均基于 RDKit 化学信息学包创建。如示例6-6所示,return_formula 工具被用来回答该问题。实际上,针对用户提出的四个查询,所需的工具均被调用,四个问题均答对,包括阿司匹林的正确二维和三维可旋转结构表示,见图6-3。正如我们之前所见,这样的成功得益于良好的工具描述和合适的模型选择。

image.png

设计一个配备定制工具集的 LangChain 代理,在所描述的查询场景下表现出显著提升。该方法的主要问题在于对新请求的扩展性,由于缺少必要工具,所开发的代理无法解决这一问题。

RDKit 自定义代理

到目前为止,我们分析了纯 GPT-3.5-turbo、GPT-4o-mini 和 O3-mini 模型以及简单链的表现,考察了专门调教的化学领域大模型的不足,以及具备预定义功能的代理在我们定义的查询上的表现。迄今为止,最佳结果是基于定制代理获得的,在该代理中我们定义了所有必需的函数。作为代理的改进,可以增加 PythonREPL 工具,用于编写和执行自定义 Python 代码。但这种方法存在一个潜在的严重问题。

请想想用于训练模型的数据。第4章提到过这个问题,讨论了基于检索增强生成(RAG)与单纯使用模型内知识库的区别:数据可能已经过时。如果训练用的文档或示例不再适用,或者缺少必要的包(比如代码运行需要额外依赖),再或者存在 Python 版本不兼容等开发者噩梦,而这些问题远未穷尽,PythonREPL 就无法正确执行生成的代码。

我们能否利用之前实现的 RAG 思路来开发一个保持最新的代理?答案是肯定的!RAG 概念只需稍作调整:

  • 下载 RDKit 文档。
  • 将文档拆分成若干块。
  • 上传这些块到向量存储。
  • 根据用户查询,检索相关示例和文档片段。

通过这种方式,只要文档存在,我们就可以支持多版本的软件包。LangChain4LifeSciencesHealthcare 代码库包含完整的抓取文档和用于更新文档的爬取脚本。展望未来,支持向量化文档的网站更可能被广泛使用。

注意
除了爬取,也可以用 inspect 包或类似工具直接访问内部的文档字符串。在 LangChain4LifeSciencesHealthcare 库中,这种方法只覆盖了 RDKit 库中部分可用方法。

示例6-7 演示了如何拆分抓取的数据,上传到向量库,并设置检索器。爬取时我们可以插入唯一的分隔符,拆分时用同样的分隔符即可。这里不需要 chunk_overlap(重叠部分),因为我们希望每个文档独立存储。接着,我们创建一个 FAISS 向量库并上传所有拆分后的文档块。

第2章介绍了不同的嵌入模型。根据任务不同,模型适用性也不同。由于我们的文档包含文本和代码,可以使用 OpenAI 的嵌入模型,它们也经过代码训练。或者尝试 instructor-xl、CodeBERT 或其他代码嵌入模型。

我们还会修改检索器的 search_type。常用的 similarity 类型是基于余弦相似度(通常默认)或欧氏距离,衡量两个元素的相似度,反映文档与查询的匹配程度。另一种是最大边际相关性(MMR),我们在第4章已使用过,它在相关性和多样性间做平衡。MMR 不仅考虑文档与查询的相似性(相关性),还考虑与已选文档的差异度(多样性)。本例中,我们希望找到略有差异的代码片段和示例,以更好地生成可执行代码。

示例6-7:LangChain 文档检索代理代码示例

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain

# 加载并拆分数据
loader = TextLoader(doc_filepath)
text_splitter = CharacterTextSplitter(
    separator=separator,
    chunk_size=1000,
    chunk_overlap=0,
    length_function=len,
    is_separator_regex=False,
    keep_separator=False,
)
texts = loader.load_and_split(text_splitter)

# 从向量库中检索
embeddings = OpenAIEmbeddings()
rdkit_db = FAISS.from_documents(texts, embeddings)
rdkit_retriever = rdkit_db.as_retriever(search_type="mmr")

提示(TIP)
这些提示能帮助系统生成正确输出:

  • 代理有时会卡在循环里反复调用同一个工具,告诉它不要连续两次以上使用同一个工具。
  • 在描述 ActionInput 时,尽量用更详细的同义表达,帮助检索更成功;并让代理在字符串开头加个 #,把它当注释处理,避免被误解为代码。
  • 如果有多个步骤,顺序也可以在提示里明确说明。
  • 要让代理优先选用检索步骤,可以提示它多用工具而不是内部知识,因为模型知识可能过时,文档更新鲜。

我们将构建两种代理:create_react_agentcreate_tool_calling_agent,两者在示例6-8中展示。它们的提示略有不同,但工具集一致:

  • rdkit_search 用于查找相关文档
  • python_repl 用于基于文档执行生成的代码

这些代理之所以被选中,是因为它们非常适合这项任务:工具调用使模型能够判断何时使用外部工具;与此同时,ReAct 框架通过结构化推理和决策过程促进了这一流程。在示例6-8中,仅给出了第二个查询的响应,因为它最能体现我们讨论的概念。请注意,create_react_agent 如何进入 AgentExecutor 链,分析查询并选择 rdkit_search,利用“molecular weight”(分子量)这一术语来查找合适的文档片段。模型对内容进行总结后,决定使用 MolWt()。随后调用 python_repl 生成代码,最终得到答案。create_tool_calling_agent 也执行了相同的操作。

示例6-8:使用 create_react_agent 和 create_tool_calling_agent

# 运行 create_react_agent
#------------------------- AI 生成内容开始 -------------------------
> 进入新的 AgentExecutor 链...
为了计算给定 SMILES 表示式 (CC(=O)OC1=CC=CC=C1C(=O)O) 化合物的分子质量,
我将使用专门用于化学信息学的 RDKit 库。具体来说,我会寻找一个能从 SMILES 字符串计算分子量的函数。

动作:rdkit_search
动作输入:# 搜索 RDKit 中可用于从 SMILES 字符串计算分子量的函数。
可能的函数名包括 `MolFromSmiles` 和 `Descriptors.MolWt`。rdkit.Chem.Descriptors.MolWt(*x, **y)¶
分子的平均分子量示例:
>>> MolWt(Chem.MolFromSmiles('CC'))
30.07
>>> MolWt(Chem.MolFromSmiles('[NH4+].[Cl-]'))
53.49...
...

接下来,我将编写 Python 代码计算给定 SMILES 表示式 (CC(=O)OC1=CC=CC=C1C(=O)O) 化合物的分子质量。

动作:python_repl
动作输入:
```python
from rdkit import Chem
from rdkit.Chem import Descriptors

# 定义 SMILES 字符串
smiles = 'CC(=O)OC1=CC=CC=C1C(=O)O'

# 将 SMILES 转换为分子对象
mol = Chem.MolFromSmiles(smiles)

# 计算分子量
molecular_weight = Descriptors.MolWt(mol)
print(molecular_weight)

180.15899999999996
我现在得到了最终答案。
最终答案:该 SMILES 表示式 CC(=O)OC1=CC=CC=C1C(=O)O 化合物的分子质量约为 180.16 克/摩尔。

链结束。
#------------------------- AI 生成内容结束 -------------------------

{'input': 'What is the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O',
'output': '该 SMILES 表示式 CC(=O)OC1=CC=CC=C1C(=O)O 化合物的分子质量约为 180.16 克/摩尔。'}

运行 create_tool_calling_agent

#------------------------- AI 生成内容开始 -------------------------

进入新的 AgentExecutor 链...

调用:rdkit_search,参数为 {'query': 'molecular mass'}
rdkit.Chem.Descriptors.MolWt(*x, **y)¶
分子的平均分子量

MolWt(Chem.MolFromSmiles('CC'))
30.07
MolWt(Chem.MolFromSmiles('[NH4+].[Cl-]'))
53.49...
...

调用:python_repl,参数为

from rdkit import Chem
from rdkit.Chem import Descriptors

# 从 SMILES 字符串创建分子对象
smiles = 'CC(=O)OC1=CC=CC=C1C(=O)O'
mol = Chem.MolFromSmiles(smiles)

# 计算分子量
molecular_weight = Descriptors.MolWt(mol)
print(molecular_weight)

180.15899999999996
该 SMILES 字符串 CC(=O)OC1=CC=CC=C1C(=O)O 表示的化合物分子质量约为 180.16 克/摩尔。

链结束。
#------------------------- AI 生成内容结束 -------------------------

{'input': 'What is the molecular mass of CC(=O)OC1=CC=CC=C1C(=O)O',
'output': '该 SMILES 字符串 CC(=O)OC1=CC=CC=C1C(=O)O 表示的化合物分子质量约为 180.16 克/摩尔。'}

对于其他查询,也获得了类似的结果,展示了该方法的潜力,因为两个代理都正确完成了所有任务。你可以更灵活地为不同软件包创建代理,而无需提前硬编码可能需要的功能。这使得你可以独立开发代理,或者像“构建多代理助理”一节所述,将其作为团队协作的一部分使用。

在从一个代理传递数据到另一个代理时,数据可能被不必要地多次处理。更好的做法是保存为本地文件,然后将文件名传递给下一个代理或用户。

表6-1总结了我们的实验结果。考虑到我们并没有提出复杂的问题,独立的大语言模型(LLMs)在从其内部知识库获取数据时出现了幻觉(hallucination),除了 o3-mini 模型例外。在基于 gpt-3.5-turbo 和 gpt-4o-mini 的 LCEL 链条中,也观察到了类似的幻觉现象。开发带有预定义工具的代理表现出色,可以被视为重复计算的解决方案。

ChemCrow 是一个有趣的代理,它可以部分解决化学任务,利用内置工具和 Python 执行工具,随着 LLM 功能的增强,通过使用最初未定义的额外功能,该代理也能实现扩展。问题是,如果单工具的 python_repl 代理质量相当,为什么还要开发这样一个代理呢?最好的、也是最具扩展性的方案,是使用不同类型的代理(create_react_agent 和 create_tool_calling_agent),将包的文档拆分并存储在向量库中,允许检索到相关文档块作为生成和执行代码的上下文。这样的方案使得你可以将 RAG 概念应用于各种与软件包相关的任务。

表6-1 代码应用结果

模型X 的化学式是什么?X 的分子质量是多少?绘制 X 的二维结构绘制 X 的三维结构
gpt-3.5-turbo+
gpt-4o-mini+
o3-mini++
LCEL 链(gpt-3.5-turbo)
LCEL 链(gpt-4o-mini)+
专门调教的化学领域 LLMs
ChemCrow(gpt-3.5-turbo)+
ChemCrow(gpt-4o-mini)+
定制工具代理++++
结合向量库的 ReAct 代理++++
结合向量库的工具调用代理++++

每次运行结果可能有所不同。在机器学习领域,一个广泛采用的做法是集成(ensemble)不同模型的结果。核心思想是正确结果通常一致,而错误通常不同。举例来说,针对经验式化学式的结果有:

  • C9H8O4(6次)
  • C10H10O4(2次)
  • C10H8O3(1次)
  • C12H10O4(1次)

在这种情况下,采用多数投票的结果是合理的。所谓“群众的智慧”正是混合专家模型(Mixture of Experts,MoE)的理论基础。

提示

我建议读者使用本测试用例或类似的测试用例对不同模型进行测试。我发现 gpt-4o 模型在类似提示下能给出更好的面向化学的结果。这是为你的应用选模型的一个可靠策略。

使用基于化学的 LLM

到目前为止,我们已经探索了将计算化学工具和 API 与文本语言模型集成开发应用的过程。包含科学知识的科学语言模型正日益成为分子发现的强大工具,涵盖从小分子到蛋白质和聚合物等复杂大分子。开源资源的使用提升了语言模型在科学研究中的可及性和实用性。

传统的分子发现方法通常遵循设计 → 合成 → 测试 → 分析(DMTA)流程,但面临诸多挑战,包括高昂的成本(每个新药可高达数十亿美元,每个新添加剂/材料数百万美元)、海量的数据筛选需求,以及假设制定中的偏差。深度生成模型能够通过精炼假设、减少偏差,识别最有潜力的研究方向,从而应对这些问题。调教过的化学语言模型在理解上下文和推理方面表现更佳,有望胜任如从 SMILES 表征预测化学实体性质等任务。

第2章介绍了不同语言模型使用的多种分词器。针对化学的模型可采用多种化学令牌格式:SMILES 字符串、自引用嵌入字符串(SELFIES)、国际化学标识符(InChI)等。这些格式可作为分子指纹,确保分子数据的准确生成和分析。

从令牌角度看,哪种格式最适用?每种格式能表达多少信息?哪些格式适合生成相似新分子?请考虑语言模型是从令牌序列中学习的。继续前,请查看表6-2,并尝试回答这些问题。

表6-2:阿司匹林、乙醇和葡萄糖的 SELFIES、SMILES 和 InChI

名称SELFIESSMILESInChI
阿司匹林 (乙酰水杨酸)[C][C][=O][O][C][C][C][=C][C][=C][C][Ring1][Branch1]CC(=O)OC1=CC=CC=C1C(=O)OInChI=1S/C9H8O4/c1-6(10)13-9-5-3-2-4-7(9)8(11)12/h2-5H,1H3,(H,11,12)
乙醇 (乙基醇)[C][C][O][H]CCOInChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3
葡萄糖[C][C][C][C][C][C][O][H][O][H][O][H][O][H][O][H][O][H][O][H]C(C1C(C(C(C(O1)O)O)O)O)OInChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2

科学研究的一个重要举措是 Generative Toolkit 4 Scientific Discovery(GT4SD) ,其使命是通过围绕科学生成模型构建开放社区,加速科学发现。该项目训练了一个模型以完成多项化学任务:

  • 化学反应预测
  • 逆合成预测
  • 文本转 SMILES
  • SMILES 转说明文字
  • 段落转操作步骤

本节将基于质量和任务执行能力测试调教过的化学 LLM。我们首先配置 ChemistryGenerator 并定义模型,如示例6-9所示。正如第2章所述,我们为每个 LLM 定义模型和分词器配对。部分被分析的 LLM 只能生成 SMILES,无法完成特定任务。新模型每周都有发布,感兴趣可以查看 LangChain4LifeSciencesHealthcare 代码库的最新版本。本节选用如下模型:

  • GT4SD/multitask-text-and-chemistry-t5-base-augm
  • GT4SD/multitask-text-and-chemistry-t5-small-augm
  • laituan245/molt5-large-caption2smiles
  • AI4Chem/CHEMLLM-2b-1_5

我们用 GT4SD 的 base 和 small 版本观察参数规模对结果质量的影响,使用 CHEMLLM-2b-1_5 观察小模型对化学知识的掌握,并用 molt5-large-caption2smiles 作为 SMILES 相关任务的对比。

示例6-9:配置化学 LLM

class ChemistryGenerator:
    def __init__(self, model, tokenizer, temperature=0.0001, do_sample=False,
                 max_length=512, num_beams=5, top_k=1):
        self.model = model.bfloat16()
        self.model.to(device)
        tokenizer.pad_token = "[PAD]"
        tokenizer.padding_side = "left"
        self.tokenizer = tokenizer
        config_kwargs = {
            "do_sample": do_sample,
            "num_beams": num_beams,
            "max_new_tokens": max_length,
        }
        if do_sample:
            config_kwargs.update({
                "top_k": top_k,
                "temperature": temperature,
                "pad_token_id": self.tokenizer.pad_token_id,
                "repetition_penalty": 1.5,
                "num_return_sequences": 1,
            })

        self.generation_config = GenerationConfig(**config_kwargs)

    def run_model(self, input_text):
        text = self.tokenizer(input_text, return_tensors="pt").to(device)
        output = self.model.generate(input_ids=text["input_ids"],
                                     generation_config=self.generation_config)
        if device == "cuda":
            output = self.tokenizer.decode(output[0], skip_special_tokens=True)
        else:
            output = self.tokenizer.decode(output[0].cpu(), skip_special_tokens=True)
        try:
            output = output.split(self.tokenizer.eos_token)[0]
            output = output.replace(self.tokenizer.pad_token, "")
            output = output.replace("<unk>", "\\")
            output = output.strip()
        except Exception as e:
            print(e)
        return output

我们先从最直接的任务开始:预测化学反应。示例用的是费舍尔酯化反应——有机酸(RCOOH)与醇(R’OH)结合生成酯(RCOOR')的过程。

# https://huggingface.co/GT4SD/multitask-text-and-chemistry-t5-base-augm
gt4sd_model_name = "GT4SD/multitask-text-and-chemistry-t5-base-augm"
model = AutoModelForSeq2SeqLM.from_pretrained(gt4sd_model_name)
tokenizer = AutoTokenizer.from_pretrained(gt4sd_model_name)

gt4sd_base_generator = ChemistryGenerator(model, tokenizer)

# https://huggingface.co/laituan245/molt5-large-caption2smiles
laituan245_model_name = "laituan245/molt5-large-caption2smiles"
tokenizer = T5Tokenizer.from_pretrained(laituan245_model_name)
model = T5ForConditionalGeneration.from_pretrained(laituan245_model_name)

laituan245_generator = ChemistryGenerator(model, tokenizer)

# https://huggingface.co/GT4SD/multitask-text-and-chemistry-t5-small-augm
gt4sd_model_name = "GT4SD/multitask-text-and-chemistry-t5-small-augm"
model = AutoModelForSeq2SeqLM.from_pretrained(gt4sd_model_name)
tokenizer = AutoTokenizer.from_pretrained(gt4sd_model_name)

gt4sd_small_generator = ChemistryGenerator(model, tokenizer)

# https://huggingface.co/AI4Chem/CHEMLLM-2b-1_5
ai4chem_model_name = "AI4Chem/CHEMLLM-2b-1_5"
model = AutoModelForCausalLM.from_pretrained(ai4chem_model_name,
                                             torch_dtype=torch.float16,
                                             device_map="auto",
                                             trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(ai4chem_model_name,
                                          trust_remote_code=True)

ai4chem_generator = ChemistryGenerator(model, tokenizer)

instance = "CC(=O)O.OCC>[H+].[Cl-].OCC"
input_text = f"Predict the product of the following reaction: {instance}"
# 期望结果: CC(=O)OCC

> gt4sd_base_generator.run_model(input_text)
>>> CCOCCOCCO...

> laituan245_generator.run_model(input_text)
>>> C1C2CN(CN2C3=C(N1)N=C(NC3=O)N)C4=CC=C(C=C4)OCC5(CC5)C6=CC=CC=C6

> gt4sd_small_generator.run_model(input_text)
>>> CCOC(=O)CCOC(C)=O

> ai4chem_generator.run_model(input_text)
>>> OCCCOCCC....

注意
LangChain4LifeSciencesHealthcare 仓库中展示了更多示例,包括其他反应及 SMILES 和文本互转。

为了简化结果,化学反应预测产物的可视化如图6-4所示。正如你所见,没有一个模型预测出了正确的结果,尽管 GT4SD/multitask-text-and-chemistry-t5-small-augm 模型的结果相当接近。

第2章中用无限猴子定理作为令牌生成的例子,即猴子随机选择下一个令牌,最终会生成正确答案。模型通常根据它们的答案进行排序,以此评估模型性能。对于科学模型,使用评分会更合适,因为有些模型可能给出正确或接近正确的答案,而有些模型则可能与事实相差甚远。例如,laituan245/molt5-large-caption2smiles 模型生成的答案值得怀疑,因为如果反应中没有含氮物质,最终产物中很可能不会含有氮原子。

image.png

让我们尝试一个逆合成任务:让模型预测合成特定产物的反应。为简化起见,我们使用之前相同的反应:

instance = "CC(=O)OCC"
input_text = f"Predict the reaction that produces the following product:\n{instance}"
# 期望结果:CC(=O)O.OCC>[H+].[Cl-].OCC

运行结果如下:

gt4sd_base_generator.run_model(input_text)

CC(=O)OCC.CC(=O)[O-]CC(=O)[O-][Pb+2][Pd].CC(=O)[O-]CC(=O)[O-][Pb+2][Pd][Pd].CC(=O)[O-]CC(=O)[O-][Pb+2][Pd].CC(=O)[O-]CC(=O)[O-][Pb+2]

laituan245_generator.run_model(input_text)

CC(=O)OC[C@@H]1C@HO

gt4sd_small_generator.run_model(input_text)

O=C([O-])[O-][K+][K+].CC(=O)OCC

ai4chem_generator.run_model(input_text)

CC(=O)OCC(=O)OCC(=O)O...

如图6-5所示,为简化起见,SMILES 答案被转换为分子结构表示。

再次说明,没有任何模型给出了正确的反应,且表现比正向反应的情况更差。这可以用训练过程以及从语言角度来看,正向过程比逆向过程更易理解来解释。

image.png

SMILES 表示既可以是完整分子,也可以是分子片段。训练于 SMILES 字符串的模型在片段信息模糊或不足以构建完整分子上下文时,可能表现不佳。如果输入缺乏结构完整性或关键上下文(例如,断开的原子、未指明的键合方式),模型可能无法推断正确的化学行为。想象下面这个例子:AB + C → ?最可能的结果是三种之一:A + BC、AC + B 或 ABC。但如果从语言角度问如何得到 ABC,可能的语言表达有很多:A + BC、AC + B、AB + C、A + B + C、ABD + C,等等。如果不了解化学反应背后的科学原理,模型只能盲目猜测各种可能的答案。

警告
虽然我之前说过,更多更长时间的训练能提升结果,但如果没有对反应的正确理解,再好的模型也无法给出理想的反应预测结果。

有机化学因其立体化学反应(分子中原子空间排列的不同)而著称。考虑到这一点,期望一维的 SMILES 令牌序列能理解三维反应的复杂性是不公平的。

我们已经看过 SMILES 到 SMILES 的转换。接下来看看如果将文本纳入任务或输出,结果是否会有所改善。我们让 LLM 根据文本描述生成一个分子:

instance = """The molecule appears as white crystals with a strong phenol odor.
Slightly soluble to soluble in water, depending on the isomer, and denser than
water. Noncombustible. Used as an intermediate in organic synthesis of dyes and
drugs. The molecule is a monochlorophenol substituted at the para position by a
chlorine atom."""
input_text = f"Write in SMILES the described molecule: {instance}"
# 期望输出:C1=CC(=CC=C1O)Cl

运行结果:

gt4sd_base_generator.run_model(input_text)

C1=CC(=CC=C1Cl)Cl

laituan245_generator.run_model(input_text)

C1=CC(=C(C=C1Cl)O)N=C2C=CC(=[NH2+])C=C2

gt4sd_small_generator.run_model(input_text)

C1=CC(=CC=C1Cl)O

ai4chem_generator.run_model(input_text)

它是单氯酚和单氯苯的成员。

这一任务是我对 laituan245/molt5-large-caption2smiles 模型最寄予厚望的,基于它的名字。两个 GT4SD 模型表现出不错的效果,毕竟该模型专门设计来解决此类任务。更重要的是,multitask-text-and-chemistry-t5-small-augm 模型成功生成了正确答案,如图6-6所示。

image.png

正如你在示例中看到的,即使输入的反应物不含氮原子,一些模型输出的结果中仍包含氮原子——表现出生成含氮有机化合物的偏好。在第10章,我会简要讨论应如何调整以减少生成模型中的这种偏差。

注意
从化学角度看,生成的 SMILES 是正确的,但从语法角度来看是不正确的。

我们将继续进行逆向任务。这次让 LLM 根据 SMILES 生成描述:

instance = "COC1=C(C=C2C3CC4=CC(=C(C=C4C(N3)CC2=C1)OC)OC)OC"
input_text = f"Caption the following smile: {instance}"
# 期望输出:
# 4-氯酚呈白色晶体,有强烈的酚类气味。根据异构体不同,水中略溶或易溶,密度大于水。不可燃。作为染料和药物有机合成中的中间体。该分子为单氯酚,在对位由氯取代。

运行结果:

gt4sd_base_generator.run_model(input_text)

该分子为氯儿茶酚,即儿茶酚中羟基对位的氢被氯取代。它是氯儿茶酚,也是单氯苯的成员。

laituan245_generator.run_model(input_text)

CC(=O)N[C@@H]1C@HO[C@H]2C@@HO

gt4sd_small_generator.run_model(input_text)

该分子为氯酚,即酚中4位的氢被氯取代。它作为细菌外来代谢物。它是氯酚,也是单氯苯的成员。其来源于酚的氢化物。

ai4chem_generator.run_model(input_text)

C1=CC(=CC=C1O)Cl

再次不意外的是,两个 GT4SD 模型表现良好,multitask-text-and-chemistry-t5-small-augm 模型生成的答案更为准确。

示例6-10 涵盖了我们给 LLM(仅限 GT4SD 模型)的最后一个任务:从文本中提取操作指令。解决此类任务对机器人实验室或标准化操作环境具有重要潜力。模型应正确识别文本中的不同命名实体。

instance = '''
该发明描述了一种合成乙酰水杨酸的新方法。
自1900年霍夫曼获得用醋酸酐和水杨酸制造乙酰水杨酸的专利以来,合成方法经历了许多改进:
Ledeler(1901年)向体系中加入硫酸以加速酯化过程。
A. Bercy(《自然》期刊,编号2977,第462页,1936年)进一步提出在乙酸作溶剂的条件下合成,系统加热至90°C一段时间后冷却至20°C。
其他作者(如 E.J.Perry,化学文摘第10卷第2121号)提出,在该温度条件下合成过程中形成酯 o-AcC6H4CO2C6H4CO2H,随后分解为乙酰水杨酸和水杨酸。
'''
input_text = f"Which actions are described in the following paragraph: {instance}"
# 期望结果:
# - ADD acetic anhydride
# - ADD salicylic acid
# - ADD sulfuric acid
# - ADDSOLVENT acetic acid
# - SETTEMPERATURE 90° C
# - SETTEMPERATURE 20° C

运行结果:

gt4sd_base_generator.run_model(input_text)

添加乙酸;
添加硫酸;
添加乙酸;
设置温度 90°C;
设置温度 20°C;
产物为酯 o-AcC6H4CO2C6H4CO2H。

gt4sd_small_generator.run_model(input_text)

添加乙酰水杨酸;
添加醋酸酐;
添加水杨酸;
设置温度 90°C;
设置温度 20°C。

两个模型都表现出潜力,但都有幻觉现象,如试剂添加顺序混淆、包含额外步骤或遗漏步骤。随着模型质量提升,命名实体识别问题将更易解决。

注意
你可能会想,既然任何 GPT、Claude、DeepSeek 或 Gemini 模型表现更佳,为何还用小型微调 LLM?虽然确实如此,但这种自动化实验室能力可使其在化学角度更具自主性和准确性。

我们已经看到化学调教 LLM 的能力。基于前述实验,multitask-text-and-chemistry-t5-small-augm 模型最有潜力。但将此类 LLM 作为 LangChain 核心虽可行,却不推荐。相反,我们可以结合 LangGraph 的思想,将特定任务委托给负责的代理。示例6-11 创建了一个 ReAct 代理,将化学调教模型作为工具,核心为基于 GPT 的模型。此架构允许核心模型的计划和工具调用能力与化学模型的知识充分发挥。其余部分,我们用示例6-6的代码开发自定义工具。

示例6-11:在 LangChain 中运行 GT4SD 化学调教 LLM

model = ChatOpenAI()

def run_chem_llm(input_text):
    return gt4sd_small_generator.run_model(input_text)

class ReturnChemLLM(BaseTool):
    name = "return_chem_llm_response"
    description = "Answers all chemistry-related questions"

    def _run(self, text: str):
        return run_chem_llm(text)

# 初始化工具
tools = [Return2DMolecule(), ReturnChemLLM()]

template = """Answer the following questions as best you can. You have access
              to the following tools: ... """
prompt = PromptTemplate.from_template(template)

agent = create_react_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 询问关于 Diels–Alder 反应
# 预期:C1=CCC=C1.C=C > C1=CC2CCC1C2
> agent_executor.invoke({"input": "Draw the molecule that is the product of the
following reaction: C1=CCC=C1.C=C"})
>>>
#------------------------- AI 生成内容开始 -------------------------
> 进入新的 AgentExecutor 链...
为了确定 C1=CCC=C1 和 C=C 的反应产物,我需要确定反应类型及其产物。

动作:return_chem_llm_response  
动作输入:C1=CCC=C1 和 C=C 的反应产物是什么?

该反应的产物是 C=CCc1ccccc1。现在,我需要绘制该分子的二维平面结构。

动作:return_2d_molecule  
动作输入:C=CCc1ccccc1

我现在得到了最终答案。

最终答案:分子 C=CCc1ccccc1 的二维平面结构已绘制。

> 链结束。
#------------------------- AI 生成内容结束 -------------------------
>>> {'input': 'Draw the molecule that is the product of the following reaction:
C1=CCC=C1.C=C',
 'output': 'The 2D flat representation of the molecule C=CCc1ccccc1 has been
 drawn.'}

如同之前的示例,绘制结果见图6-7。我们用工具调用 LLM 来决定计划和使用的工具,尽管化学调教 LLM 没有生成正确结果,整个应用仍按预期运行。

image.png

使用较小模型来解决特定任务的策略具有巨大潜力。它们不仅可以更快、更便宜地训练和更新,而且专门任务的质量可能比大型模型更高。根据论文《Unifying Molecular and Textual Representations via Multi-task Language Modelling》,小模型的参数量为6000万,而基础模型有2.2亿参数。在多次实验中,我们发现小模型表现优于基础模型,这可能显示出基础模型存在过拟合的迹象。

目前化学调教的大语言模型质量不足可能有多种原因。从架构角度看,小模型的调教存在一定限制。更广泛的视角是知识蒸馏路径,即较小的学生模型模仿大型教师模型的行为并内化其知识。学生模型的主要任务是通过观察和学习教师模型对不同输入的预测、调整和响应,复制教师的输出。如果教师模型具备推理能力,则这种能力可以转移给学生模型,进而取得更好效果。

注意
第8章将介绍条件变分自编码器及其在分子生成中的应用。

另一个重要因素是数据。在训练各种文本时,语言模型学习文本令牌之间的模式。化学符号的变异性无法与任何语言相比,且数据量也更加有限。此外,如第2章所述,生成器从最可能的令牌列表中选择下一个令牌,而在化学中,通常只有一个正确的下一个令牌。

调教过的化学语言模型能够基于训练数据之外的复杂化学性质和结构做出准确预测,其能力可与其他算法和机器学习引擎相媲美。这种能力可能为材料性质、合成技术和设计原理提供有价值的见解。另一潜在应用是解码深度学习模型的预测,提供对分子子结构及其影响的更明确解释。

在化学应用中使用基于文本的 LLM

到目前为止,本章探讨了如何利用 LLM 生成化学相关的结果。接下来我们来看一个不那么专注于化学本身但潜力巨大的应用。

第5章讨论了如何将 LLM 用于科学领域。在多个应用中,我们以不同方式使用了文本能力:从重度(文本摘要、假设检索)到轻度(生成最终回答,作为内部计划和沟通的润滑剂)。

优质数据胜过大数据。高质量数据比单纯拥有大量数据更有价值,因为准确且相关的数据能带来更好的洞察和决策。最重要的是,优质数据能帮助机器学习模型更好地学习,做出更可靠的预测。在第二个案例中,我们将开发一个简单的 LangChain 应用,用于检测化学数据库中的拼写错误。示例6-12展示了如何调用 PubChem API 来检查可能的拼写错误物质。

我们将创建一个拼写错误生成链,使用带有提示的 LLM,指示模型扮演专业编辑和拼写错误捕捉者的角色。然后调用该链,生成单词 “ethyl” 的潜在拼写错误列表,并返回可能的错误拼写。之后,我们连接到 PubChem 数据库 API,构造搜索查询,查找与拼写错误匹配的物质。

示例6-12 显示了我们找到的拼写错误及对应匹配数:

  • ethly (2453 个匹配)
  • ehtyl (53 个匹配)
  • etyl (23 个匹配)
  • ehtly (10 个匹配)
  • etyhl (1 个匹配)

示例6-12:生成 “ethyl” 拼写错误并检索 PubChem 数据

# 设置链以生成可能的拼写错误
prompt = ChatPromptTemplate.from_messages(
    [        ("system", "You are a professional editor and typo-catcher"),        ("placeholder", "{chat_history}"),        ("human", "{input}"),        ("placeholder", "{agent_scratchpad}"),    ]
)

typo_chain = prompt | llm | StrOutputParser()

# 示例:查找 PubChem 中的拼写错误物质
word = "ethyl"
typo_llm_response = typo_chain.invoke(f"""
    Return a semicolon-separated list of 10 most possible typos for word {word}.
    The response should contain only possible typos!
    Don't include initial word {word}
    Avoid adding duplicates."""
)
typo_list = [x.strip() for x in typo_llm_response.replace(".", "").split(";")]
# ['ehtyl','etyl','ethly','eythl','ehtil','etyhl','ethil','eylth','ehtly', 'etlhy']

# 调用 PubChem 并检索数据的方法
def get_pubchem_data(subword):
    response = requests.get(
        'https://pubchem.ncbi.nlm.nih.gov/sdq/sdqagent.cgi?infmt=json&outfmt=json&query={%22select%22:...[{%22*%22:%22'+subword+'%22}]}...}',
        cookies={}, headers={},
    )
    return response

typo_dict = {}
for subword in typo_list:
    response = get_pubchem_data(subword)
    total_count = response.json()['SDQOutputSet'][0]['totalCount']
    if total_count:
        typo_dict[subword] = total_count

print(typo_dict)
# {'ehtyl': 53, 'etyl': 23, 'ethly': 2453, 'etyhl': 1, 'ehtly': 10}

如你所见,PubChem 数据库中确实存在拼写错误,如图6-8所示。这可能导致用户无法找到正确化合物,产生错误的搜索结果。对于潜在的 RAG 流水线亦是如此。发现所有错误有助于提升生成式 AI 应用和化学调教 LLM 的质量。

这个拼写错误处理应用能改善化学信息检索系统的搜索成功率。当研究人员、学生或行业专业人士检索复杂或陌生名称的化合物时,拼写错误常见且会导致搜索失败。此外,利用该应用分析常见拼写错误,还能为科学术语和化学化合物中的命名模式及语言关系提供有价值的见解。

image.png

总结

本章旨在展示如何构建和使用 LangChain 应用来解决化学相关问题,探讨生成式 AI 与化学的交叉领域,重点介绍 LangChain 和化学调教大语言模型(LLMs)如何变革化学研究与应用。

本章将生成式 AI 在化学中的应用分为三大类:

  • 基于代码的应用
    生成代码以自动化化学研究任务
  • 基于化学的应用
    使用专门的语言模型创建分子结构和预测化学反应
  • 基于文本的应用
    处理人类语言,帮助科学家高效获取论文信息

我们开发并分析了多个应用,如 ChemCrow 和 CACTUS,这些应用利用 LLM 解析和生成化学信息,同时保持安全协议,防止执行危险操作。通过对不同模型(gpt-3.5-turbo、gpt-4o-mini、o3-mini)进行性能基准测试,揭示了当前生成式 AI 在化学任务中的优势与局限。

本章展示了配备合适工具时,强大应用的可能性,演示了如何基于最新文档开发生成并执行代码的应用。

此外,本章探讨了对化学调教 LLM 在化学反应预测和逆合成等任务上的测试,暴露了目前的限制,尤其是如何通过一维令牌序列捕捉化学反应复杂三维特性上的不足。

LLM 在化学领域影响最大的或许是其代码能力:支持应用开发、调用必要工具、成功生成与调试复杂仿真代码,体现其作为计算化学强大工具的地位。本章也展示了将 LLM 融入化学研究与工业的一个极佳途径——构建拥有大型化学包知识库的指导型 LLM。像 Cursor 这类公司(目前估值数十亿美元的初创企业)正采用类似方法,支持上传软件包文档或自有代码库。

未来章节将基于这些基础,展示更多 LangChain 流水线和 LangGraph 团队在生命科学各领域的案例,进一步体现生成式 AI 在化学及相关领域的变革潜力。下一章我们将聚焦于构建生物研究多功能团队,并对 DeepSeek 模型进行生物数据微调。