163

大语言模型与药物发现:让 LLM 做编排,不要让它装成实验仪器

LLM 在药物发现中适合做编排与信息处理,不适合装成实验仪器。这篇给出合理的用法边界与落地场景。

大语言模型在药物发现中的位置容易被误解。它的合理定位是「编排者与信息处理者」——把工具串起来、把文献读进来、把结果整理出去;而不是「预测器」——直接回答分子的活性或性质。

LLM 能做与不能做

任务 适合度 说明
文献检索与综述 很适合 需要人工核对引用
从文献/专利抽取结构化信息 很适合 如提取活性数据表
编排计算工具链 很适合 调用 RDKit、对接、模型
写代码与数据分析 很适合 加速日常工作
生成实验方案草稿 可以 必须专家审核
解释计算结果 可以 需要提供准确的上下文
预测分子活性 不适合 应该调用专门模型
预测 ADMET 性质 不适合 同上
生成有效的 SMILES 不可靠 常生成价键错误的结构
判断分子相似性 不适合 应该算指纹
作为事实来源 不适合 会编造文献与数据

为什么不该让 LLM 直接预测分子性质

# 原因一:【它没有见过足够的定量数据】
#   训练语料中的分子数据大多是文本描述,
#   而非结构化的活性测定值
#
# 原因二:【无法处理精确的结构差异】
#   一个甲基的位置变化可能让活性差 100 倍
#   而 LLM 对 SMILES 的「理解」是 token 级的
#
# 原因三:【会自信地编造】
#   问「这个分子的 logP 是多少」
#   → 它会给出一个看似合理的数字
#   → 但这个数字没有依据
#   → 【比不回答更危险】
#
# 原因四:【有更好的工具】
#   logP:  RDKit Crippen 计算,秒级,有明确定义
#   活性:  专门训练的 QSAR 模型
#   相似性: 指纹 + Tanimoto
#   → 【用确定性工具替代概率性猜测】
#
# 【正确做法】:
#   让 LLM 【调用】这些工具,而非【模仿】它们

# 反例(不要这样做):
#   prompt = "这个分子 CC(=O)Nc1ccc(O)cc1 的 logP 是多少?"
#
# 正例:
#   让 LLM 生成并执行代码:
#   from rdkit import Chem
#   from rdkit.Chem import Crippen
#   mol = Chem.MolFromSmiles("CC(=O)Nc1ccc(O)cc1")
#   print(Crippen.MolLogP(mol))

编排模式:最有价值的用法

# 让 LLM 作为「协调者」,调用确定性工具

import json
from rdkit import Chem
from rdkit.Chem import Descriptors, Crippen, QED

# ---- 定义工具 ----
def calc_properties(smiles: str) -> dict:
    """计算分子的物化性质"""
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return {"error": "无效的 SMILES"}
    return {
        "MW": round(Descriptors.MolWt(mol), 2),
        "logP": round(Crippen.MolLogP(mol), 2),
        "TPSA": round(Descriptors.TPSA(mol), 2),
        "HBD": Descriptors.NumHDonors(mol),
        "HBA": Descriptors.NumHAcceptors(mol),
        "RotB": Descriptors.NumRotatableBonds(mol),
        "QED": round(QED.qed(mol), 3),
    }

def check_alerts(smiles: str) -> dict:
    """检查结构警示"""
    from rdkit.Chem import FilterCatalog
    params = FilterCatalog.FilterCatalogParams()
    params.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.PAINS)
    catalog = FilterCatalog.FilterCatalog(params)
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return {"error": "无效的 SMILES"}
    matches = catalog.GetMatches(mol)
    return {"pains_alerts": [m.GetDescription() for m in matches]}

def predict_activity(smiles: str) -> dict:
    """调用训练好的 QSAR 模型"""
    # 【关键】:这里调用的是真正训练过的模型
    pred, unc = qsar_model.predict_with_uncertainty(smiles)
    sim = applicability_check(smiles)
    return {"pActivity": float(pred), "uncertainty": float(unc),
            "max_train_similarity": float(sim),
            "reliable": bool(sim > 0.4)}

TOOLS = {
    "calc_properties": calc_properties,
    "check_alerts": check_alerts,
    "predict_activity": predict_activity,
}

# ---- LLM 决定调用哪些工具,并解释结果 ----
# 用户:「帮我评估这批分子,找出最有希望的」
# LLM:
#   1) 对每个分子调用 calc_properties 与 check_alerts
#   2) 过滤掉有警示、性质超范围的
#   3) 对剩余的调用 predict_activity
#   4) 【整合结果,用自然语言解释权衡】
#
# 【LLM 的价值在第 4 步】:
#   把多维度的数字整理成人能快速理解的判断
#   而所有数字本身来自确定性工具

文献与专利信息抽取

  • 这是 LLM 在药物发现中最成熟的应用:从非结构化文本中提取化合物结构、活性数据、实验条件;
  • 典型场景:从专利的 Markush 结构与实施例表格中提取化合物与活性;从文献方法部分提取实验条件;
  • 必须配套的验证
    • 抽取的 SMILES 必须用 RDKit 验证
    • 数值要检查单位与量级是否合理;
    • 抽样人工核对——建立准确率的基线;
    • 记录来源(哪篇文献、哪一页、哪个表)以便追溯。
  • 化学结构识别(OCSR):从图片中识别化学结构是专门的任务,应该用 DECIMER、MolScribe 等专用工具,而非通用 LLM。

使用 LLM 的纪律

# 1) 【任何事实性输出都要验证】
#    文献引用 → 核对是否真实存在
#    数值 → 用工具重算
#    化学结构 → RDKit 解析
#
# 2) 【提供准确的上下文】
#    与其让它「回忆」,不如把资料给它
#    → 检索增强(RAG)比纯生成可靠得多
#
# 3) 【明确它的角色】
#    在提示中说明:
#    「你的任务是调用工具并整理结果,
#      不要凭记忆回答数值问题」
#
# 4) 【保留人的决策权】
#    LLM 给出建议与整理,
#    决策由人做出并记录
#
# 5) 【数据安全】
#    【专有的分子结构、靶点信息
#     不应发送到外部 API】
#    → 敏感场景用本地部署的模型
#    → 或先脱敏
#
# 6) 【记录可追溯】
#    保存提示、输出、调用的工具与结果
#    → 决策要能解释与复现

# 【一个实用的自检问题】:
#   「如果 LLM 这次的输出是错的,
#     我会怎么发现?」
#   → 答不上来,就说明缺少验证环节

关键要点

  • 让 LLM 调用工具,而非模仿工具——logP 用 RDKit 算,活性用 QSAR 模型预测;
  • 它会自信地编造数值与文献,这比不回答更危险;
  • 最成熟的应用是文献与专利的信息抽取,但抽取结果必须用 RDKit 验证并抽样人工核对;
  • 专有分子结构不应发送到外部 API;每个输出都要能回答「错了我怎么发现」。

延伸资源