化学分子Agent要解决的是一个非常实际的问题:让模型不仅会生成分子式,还能围绕分子完成计算、验证和路线设计。单独使用大语言模型时,它可能输出一个结构合理的SMILES,却无法保证logP数值准确,更难以判断某一步反应是否需要保护基。把性质预测工具与逆合成引擎接入代理之后,系统每走一步都能从确定性工具获得反馈,再继续下一步推理。这种工作方式与普通问答模型不同,它把大语言模型定位为调度者而非化学数据库。

一个完整的分子Agent通常包含规划器、工具集和记忆模块。规划器理解用户输入,把目标拆成性质计算、合成搜索、原料验证等子任务;工具集提供可由程序执行的化学计算能力;记忆模块保存SMILES、中间产物、打分结果和已经尝试过的路径,避免重复计算。下面从这三个部分展开。
一、化学分子Agent的核心组成
大语言模型本身并不掌握精确的分子描述符,也没有可靠的逆合成知识。若让它直接回答阿司匹林的logP,数值来源可能是训练数据中的近似值,而不是针对给定结构的计算结果。化学分子Agent的解决方式是把计算外包给专业工具,让模型根据工具返回的数据继续作答。
工具接口的设计非常关键。一个性质预测工具不应该只返回一个浮点数,最好同时给出单位、计算方法和必要的警告。例如使用RDKit计算分子量时,返回结果可以包括mw、logp、tpsa以及SMILES是否有效的标记。这样模型在后续推理中能判断该分子的可合成性是否受到结构问题影响。
记忆模块同样重要。逆合成搜索过程中会产生大量中间体,如果每个中间体都重新调用性质预测工具,成本会很高。将已经计算过的描述符和合成评分存下来,可以有效减少工具调用次数,并让模型在长链路规划中保持上下文一致。
二、分子性质预测:工具接入与结果验证
分子性质预测大致分为定量任务和定性任务。定量任务包括脂水分配系数logP、溶解度、熔点、分子量等;定性任务包括毒性预测、血脑屏障通透性、代谢稳定性等。RDKit可以快速计算大量结构描述符,但更复杂的毒性或吸收性质通常需要图神经网络模型。
对接到Agent时,可以用分子指纹作为特征,也可以直接使用预训练模型。RDKit生成Morgan指纹后,模型输出一个0到1之间的风险分数。工具返回时最好注明阈值和训练数据分布,否则模型可能把一个输出0.6的结果错误解释为确定阳性。
from rdkit import Chem
from rdkit.Chem import Descriptors, Crippen
def calc_props(smiles: str) -> dict:
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return {"error": "invalid SMILES", "valid": False}
return {
"valid": True,
"mw": round(Descriptors.MolWt(mol), 2),
"logp": round(Crippen.MolLogP(mol), 3),
"hbd": Descriptors.NumHDonors(mol),
"hba": Descriptors.NumHAcceptors(mol),
"tpsa": round(Descriptors.TPSA(mol), 2),
}
这段代码可以直接作为Agent的一个工具函数。模型拿到结构化结果后,不需要死记logP数值,而可以集中精力做判断,比如比较两个候选分子的类药性,或者根据分子量判断是否适合后续合成。
三、合成规划:逆合成搜索与约束
合成规划的任务是从目标分子出发,逐步找到可以购买或容易制备的起始原料。直接让模型从正反应方向推测路线并不现实,因为每一步反应都有大量可能产物。逆向搜索能大幅缩小范围:对目标分子应用逆合成模板,得到前体,再对前体继续拆解,直到满足终止条件。
真实系统通常会为每一条路径打分,考虑模板适用性、步骤数、原料成本、有毒试剂使用情况和预计产率。简单做法是使用贪心最佳优先搜索,每一步保留评分最高的一批节点;更复杂的方法会加入蒙特卡洛树搜索或强化学习。无论使用哪种搜索策略,Agent都要把搜索状态贯穿在上下文中,避免模型凭感觉修改已经验证过的路线。
from dataclasses import dataclass
@dataclass
class Node:
smiles: str
depth: int
score: float
parent: object
def best_first_search(target, templates, is_buyable, max_depth=5, beam_size=20):
root = Node(target, 0, 0.0, None)
frontier = [root]
for _ in range(max_depth):
if not frontier:
break
frontier.sort(key=lambda n: n.score, reverse=True)
frontier = frontier[:beam_size]
next_frontier = []
for node in frontier:
children = apply_templates(node.smiles, templates)
if not children:
continue
for child_smiles, step_score in children:
new_score = node.score + step_score
if new_score > 6.0:
continue
child = Node(child_smiles, node.depth + 1, new_score, node)
if is_buyable(child_smiles):
return child
next_frontier.append(child)
frontier = next_frontier
return None
上面的代码是一个简化版搜索流程,apply_templates可以替换为ASKCOS或本地规则库。实际部署时,还要处理工具超时、空结果和路线冲突。Agent需要把这些异常信息也带回给语言模型,而不是隐藏错误后继续生成虚假路径。
四、最小可用分子Agent的实现与评估
把性质预测和合成规划接入同一个Agent时,可以采用ReAct风格的循环。模型每轮输出一个工具调用请求,系统执行后把结果追加到消息列表。工具调用格式可以使用JSON,也可以使用大模型平台提供的函数调用接口。对开发者来说,关键是让模型明确知道有哪些工具、每个工具需要什么参数。
下面是一个简化版的调度函数,工具字典和工具函数通过参数传入,便于测试和替换模型。
import json
def run_agent(user_query, model, tools, max_steps=6):
messages = [{"role": "user", "content": user_query}]
for step in range(max_steps):
reply = model.generate(messages)
action = parse_tool_call(reply)
if action is None:
return reply
name, args = action
if name not in tools:
messages.append({"role": "user", "content": "unknown tool"})
continue
result = tools[name](**args)
messages.append({"role": "assistant", "content": reply})
messages.append({
"role": "tool",
"name": name,
"content": json.dumps(result, ensure_ascii=False)
})
return "max steps reached"
评估化学分子Agent不能只看文本是否流畅。性质预测工具需要单独计算MAE、RMSE或AUC指标;合成规划需要统计路线成功率、平均步数、可购原料覆盖率和化学合理性。模型在多步推理中容易忽略之前工具返回的警告,因此可以在提示中要求它在最终答案前复述关键前提。
当前主要局限来自三个方面:语言模型对化学规则的幻觉、工具调用的延迟以及搜索空间过大时无法彻底探索。改进办法包括对工具返回做强约束校验、用缓存减少重复计算、以及对高风险路线加入人工确认节点。一个稳定的分子Agent不应该追求一次生成完美答案,而是通过小步验证逐步逼近可用结果。