从“会聊天”到“会干活”:一个小白也能跑起来的 Agent 入门
tool"""Args:""""error": "SMILES 无法被 RDKit 正确解析。},qed_level = "较高"qed_level = "中等"else:qed_level = "偏低"judgement = "初步看具备较好的口服小分子药物性潜力"judgement = "存在一定口服药物性风险,需要进一步优化"else:judgement = "口服小分子药物性风险较高"},
很多人第一次听到 Agent,会觉得这是一个很高级、很玄的概念。有人说它是智能体,有人说它是自动化助手,还有人说它以后能替代很多工作。说法很多,听起来也很热闹。
但如果我们把这些大词先放一边,Agent 其实可以用一句很朴素的话理解:
Agent 就是一个会自己判断下一步该怎么做,并能调用工具完成任务的程序。
普通聊天机器人更多是在“回答问题”。而 Agent 不只是回答,它还可以判断:这个问题要不要查资料?要不要算一下?要不要调用某个工具?工具算完之后,又应该怎样把结果解释给人听?
这就像我们平时请一个朋友帮忙。
你问:“我这个月花销是不是超了?”
一个只会聊天的人可能说:“你要注意理财。”
一个真正能帮忙的人会说:“你把账单给我,我先帮你分类,再算总支出,最后告诉你哪里花多了。”
Agent 的差别就在这里:它不只是说,它还要会做。
这篇文章基于两个小项目来讲 Agent 入门:
第一个项目:不用任何 Agent 框架,手写一个最小药学 Agent。
第二个项目:使用 LangGraph,构建一个具有短期记忆能力的药学 Agent。
这两个项目都围绕同一个任务展开:用户输入一个小分子的 SMILES,Agent 调用 RDKit 计算分子性质,然后用中文解释这个分子的口服药物性。
这里的重点不是药学本身,而是通过一个真实小例子,看懂 Agent 是怎么搭起来的。
一、Agent 到底比普通聊天机器人多了什么?
我们先从一个生活例子说起。
假如你问一个普通聊天机器人:
我想做番茄炒蛋,需要准备什么?
它可以直接回答:番茄、鸡蛋、葱、盐、油。
这个问题不需要工具,因为它靠常识就能回答。
但如果你问:
我冰箱里有两个番茄、三个鸡蛋、一盒豆腐,帮我算一下能做几道菜,并估计热量。
这时只靠聊天就不够了。它最好能做几件事:
-
先判断这个问题涉及食材和热量计算;
-
调用食材热量表或计算工具;
-
根据计算结果给出建议;
-
如果你继续问“刚才哪道菜热量最低”,它还要记得前面说过什么。
这就是 Agent 的基本思路。
对应到代码里,一个最小 Agent 通常包括四个东西:
用户问题
↓
大模型判断要不要调用工具
↓
如果需要,就调用工具
↓
大模型根据工具结果组织最终回答
所以,Agent 不是“大模型自己变成了万能专家”。更准确地说,Agent 是把大模型、工具、程序流程和记忆组合到一起。
可以把它理解为:
Agent = 大模型 + 工具 + 流程控制 + 记忆
其中:
-
大模型负责理解问题和组织语言;
-
工具负责做确定性的事情,比如计算、检索、查数据库;
-
流程控制负责决定下一步走哪里;
-
记忆负责保存上下文,让它不要每一轮都像第一次见你。
二、第一个项目:不用框架,写一个最小 Agent
我们先不要急着用 LangGraph、LangChain 这些框架。初学时,最重要的是先看清楚 Agent 的骨架。
第一个项目的目标很简单:
用户输入一个 SMILES,Agent 判断是否需要调用 RDKit。如果需要,就计算分子性质,再让大模型解释结果。
整体流程如下:
用户输入问题
↓
LLM 先做判断:是否需要调用 analyze_smiles 工具
↓
如果需要,程序调用 RDKit 工具
↓
RDKit 返回分子性质
↓
LLM 根据真实结果生成中文解释
这里的例子使用本地 Ollama 模型和 RDKit 工具。
三、准备环境
1. 创建 Python 环境
先创建一个新的 conda 环境:
conda create -n pharm_agent python=3.10 -c conda-forge rdkit -y
conda activate pharm_agent
如果 RDKit 没有正常安装,可以再执行:
conda install conda-forge::rdkit
安装 requests,用来调用本地 Ollama 接口:
pip install requests
2. 安装并准备 Ollama
Ollama 是一个可以在本地运行大模型的工具。安装好 Ollama 后,可以下载一个模型,例如:
ollama pull qwen2.5:7b
查看本地已经下载的模型:
ollama list
如果模型已经存在,就可以开始写代码。
四、完整代码:最小药学 Agent
新建一个文件:
touch pharm_agent_minimal.py
然后写入下面的代码。
1. 导入依赖
import json
import os
import re
import requests
from rdkit import Chem
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED
这些包分成三类:
-
json、re:用来解析模型输出; -
requests:用来请求 Ollama 本地模型; -
rdkit:用来计算分子性质。
2. 设置 Ollama 地址和模型名称
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434/api/chat")
MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:7b")
这里默认调用本地 Ollama 的 chat 接口,模型默认是 qwen2.5:7b。
如果你想换模型,也可以在终端里设置:
export OLLAMA_MODEL=llama3.1:8b
Windows PowerShell 可以写成:
$env:OLLAMA_MODEL="llama3.1:8b"
3. 封装大模型调用函数
def ollama_chat(messages, json_mode=False, temperature=0.0):
"""
调用本地 Ollama 模型。
json_mode=True 时,要求模型尽量返回 JSON。
"""
payload = {
"model": MODEL,
"messages": messages,
"stream": False,
"options": {
"temperature": temperature
}
}
if json_mode:
payload["format"] = "json"
response = requests.post(OLLAMA_URL, json=payload, timeout=120)
response.raise_for_status()
data = response.json()
return data["message"]["content"]
这个函数的作用很简单:把消息发给本地模型,然后拿回模型回复。
这里有一个小细节:
"temperature": 0.0
温度越低,模型回答越稳定。Agent 在做工具调用判断时,最好稳定一点,不要太“有创意”。
4. 解析模型返回的 JSON
def safe_json_loads(text):
"""
尽量从模型输出中解析 JSON。
"""
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r"\{.*\}", text, re.DOTALL)
if not match:
raise ValueError(f"模型没有返回合法 JSON:
{text}")
return json.loads(match.group(0))
为什么需要这个函数?
因为大模型有时候不太老实。你让它只输出 JSON,它可能会多说一句“好的,下面是结果”。这对人来说没问题,但对程序来说就很麻烦。
所以这里写了一个保险函数:如果不能直接解析,就尝试从模型输出里提取 {...} 这一段。
5. 编写 RDKit 工具函数
这个函数就是 Agent 的工具。
def analyze_smiles(smiles):
"""
药学工具函数:
使用 RDKit 计算一个小分子的基础药物性指标。
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return {
"ok": False,
"error": "SMILES 无法被 RDKit 正确解析",
"smiles": smiles
}
mw = Descriptors.MolWt(mol)
logp = Crippen.MolLogP(mol)
hbd = Lipinski.NumHDonors(mol)
hba = Lipinski.NumHAcceptors(mol)
tpsa = rdMolDescriptors.CalcTPSA(mol)
rot_bonds = Lipinski.NumRotatableBonds(mol)
qed = QED.qed(mol)
fraction_csp3 = rdMolDescriptors.CalcFractionCSP3(mol)
lipinski_rules = {
"MW <= 500": mw <= 500,
"LogP <= 5": logp <= 5,
"HBD <= 5": hbd <= 5,
"HBA <= 10": hba <= 10,
}
lipinski_violations = [
rule for rule, passed in lipinski_rules.items() if not passed
]
veber_rules = {
"TPSA <= 140": tpsa <= 140,
"Rotatable bonds <= 10": rot_bonds <= 10,
}
veber_violations = [
rule for rule, passed in veber_rules.items() if not passed
]
if qed >= 0.67:
qed_level = "较高"
elif qed >= 0.49:
qed_level = "中等"
else:
qed_level = "偏低"
if len(lipinski_violations) <= 1 and len(veber_violations) == 0:
oral_druglike_judgement = "初步看具备较好的口服小分子药物性潜力"
elif len(lipinski_violations) <= 2:
oral_druglike_judgement = "存在一定口服药物性风险,需要进一步优化"
else:
oral_druglike_judgement = "口服小分子药物性风险较高"
return {
"ok": True,
"smiles": smiles,
"properties": {
"molecular_weight": round(mw, 2),
"logP": round(logp, 2),
"hbd": hbd,
"hba": hba,
"tpsa": round(tpsa, 2),
"rotatable_bonds": rot_bonds,
"qed": round(qed, 3),
"qed_level": qed_level,
"fraction_csp3": round(fraction_csp3, 3),
},
"lipinski": {
"rules": lipinski_rules,
"violations": lipinski_violations,
"num_violations": len(lipinski_violations),
},
"veber": {
"rules": veber_rules,
"violations": veber_violations,
"num_violations": len(veber_violations),
},
"judgement": oral_druglike_judgement,
"note": "该结果仅用于计算药物化学初筛,不代表真实活性、毒性、成药性或临床有效性。"
}
这个工具函数只做一件事:根据 SMILES 算分子性质。
它不负责写漂亮的解释,也不负责判断用户意图。它就像厨房里的电子秤,只负责称重,不负责教你做菜。
6. 注册工具
TOOLS = {
"analyze_smiles": analyze_smiles
}
这里把工具放进一个字典里。后面大模型如果说要调用 analyze_smiles,程序就从这个字典里找到对应函数。
7. 编写 Agent 主函数
这是整个最小 Agent 的核心。
def agent_run(user_question):
"""
最小 Agent:
1. 让 LLM 决定是否调用工具;
2. 如果调用工具,则执行工具;
3. 再让 LLM 基于工具结果生成药学解释。
"""
planner_system_prompt = """
你是一个药学研究 Agent。你可以根据用户问题决定是否调用工具。
你只有一个工具:
工具名:analyze_smiles
作用:根据 SMILES 计算小分子的基础药物性指标,包括分子量、LogP、HBD、HBA、TPSA、可旋转键、QED、Lipinski 和 Veber 规则。
你必须只输出 JSON,不要输出其他文字。
如果用户提供了 SMILES,并且希望分析药物性、成药性、口服性质、类药性、小分子性质,则输出:
{
"action": "analyze_smiles",
"arguments": {
"smiles": "用户提供的SMILES"
}
}
如果用户没有提供 SMILES,或者不需要工具,则输出:
{
"action": "final",
"answer": "请用户提供SMILES,或直接回答用户问题"
}
不要编造计算结果。
"""
planner_messages = [
{"role": "system", "content": planner_system_prompt},
{"role": "user", "content": user_question}
]
plan_text = ollama_chat(planner_messages, json_mode=True)
plan = safe_json_loads(plan_text)
action = plan.get("action")
if action == "final":
return plan.get("answer", "请提供更具体的问题。")
if action not in TOOLS:
return f"未知工具:{action}"
tool_args = plan.get("arguments", {})
tool_result = TOOLS[action](**tool_args)
reporter_system_prompt = """
你是一个药物化学与 AIDD 研究助手。
请根据工具返回的真实计算结果,用中文生成研究解读。
要求:
1. 先给出核心判断;
2. 再解释关键指标;
3. 指出潜在风险;
4. 给出下一步研究建议;
5. 不要声称该分子一定有效;
6. 不要给临床用药建议。
"""
reporter_messages = [
{"role": "system", "content": reporter_system_prompt},
{"role": "user", "content": f"用户原始问题:{user_question}"},
{"role": "user", "content": f"工具计算结果:{json.dumps(tool_result, ensure_ascii=False, indent=2)}"}
]
final_answer = ollama_chat(reporter_messages, json_mode=False)
return final_answer
这段代码可以拆成两次大模型调用。
第一次调用大模型,是让它做“计划”:
这个问题要不要调用工具?
如果要,调用哪个工具?参数是什么?
第二次调用大模型,是让它做“汇报”:
工具结果已经出来了,请用人能听懂的话解释一下。
这就是最小 Agent 的关键。
它不是一上来就让大模型直接回答,而是先让模型决定是否要借助工具。
8. 添加命令行运行入口
if __name__ == "__main__":
print("药学最小 Agent 已启动。输入 exit 退出。")
print("示例:请分析阿司匹林的口服药物性,SMILES: CC(=O)Oc1ccccc1C(=O)O")
print("-" * 80)
while True:
question = input("
你:").strip()
if question.lower() in ["exit", "quit", "q"]:
break
try:
answer = agent_run(question)
print("
Agent:")
print(answer)
except Exception as e:
print("
运行出错:")
print(e)
运行脚本:
python pharm_agent_minimal.py
输入测试问题:
请分析阿司匹林的口服药物性,SMILES: CC(=O)Oc1ccccc1C(=O)O
你会看到 Agent 先调用 RDKit,再生成类似这样的回答:
核心判断:
阿司匹林初步看具备较好的口服小分子药物性潜力。
关键指标解释:
分子量、LogP、氢键供体、氢键受体、TPSA、可旋转键等指标基本符合 Lipinski 和 Veber 规则。
潜在风险:
这些指标只能说明基础药物性较好,不能代表真实活性、毒性和临床有效性。
下一步建议:
可以进一步结合活性实验、毒性评估、代谢稳定性和药代动力学研究。
再测试咖啡因:
请分析咖啡因是否具备口服小分子药物性,SMILES: Cn1cnc2c1c(=O)n(C)c(=O)n2C
这时 Agent 也会调用同一个工具,然后给出咖啡因的分子性质解释。
五、这个最小 Agent 的问题:它没有记忆
第一版 Agent 已经能工作了,但它有一个明显问题:它不记得前面说过什么。
比如你先问:
请分析阿司匹林的口服药物性,SMILES: CC(=O)Oc1ccccc1C(=O)O
它回答完之后,你再问:
刚才这个分子的主要风险是什么?
它很可能回答不上来,或者要求你重新提供 SMILES。
这不是因为它笨,而是因为我们第一版程序没有保存上下文。每次提问,都是一个新的请求。程序没有把上一轮的问题、工具结果和回答保存下来。
这就像你去窗口办业务,刚递完材料,工作人员立刻把材料扔了。你下一句问“刚才那个材料有什么问题”,他当然不知道。
所以第二版 Agent 要解决的问题就是:让它有短期记忆。
六、第二个项目:使用 LangGraph 构建有记忆的 Agent
LangGraph 可以把 Agent 设计成一个“图”。
这个图里有节点,有边,也有条件判断。
在我们的例子里,图很简单:
START
↓
assistant 节点
↓
判断是否需要调用工具
↓ ↓
tools 节点 直接结束
↓
assistant 节点总结工具结果
可以把它理解成一个办事流程:
-
assistant先接待用户; -
如果发现需要计算,就把任务交给
tools; -
tools算完后,把结果交回assistant; -
assistant再把结果解释给用户。
这比第一版更清楚,也更适合扩展。
七、安装 LangGraph 相关包
在原来的环境中安装:
pip install -U langgraph langchain langchain-core langchain-ollama
准备一个支持工具调用的本地模型,例如:
ollama pull llama3.1:8b
这里使用 llama3.1:8b,主要是因为它在 Ollama 中比较常用,也支持 tool calling,适合做本地 Agent 实验。
八、完整代码:LangGraph 药学 Agent
新建文件:
touch pharm_langgraph_agent.py
1. 导入依赖
import json
import os
from typing import Any, Dict
from rdkit import Chem
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED
from langchain_ollama import ChatOllama
from langchain_core.tools import tool
from langchain_core.messages import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import InMemorySaver
这里比第一版多了 LangGraph 和 LangChain 相关组件。
核心组件有几个:
-
ChatOllama:调用 Ollama 模型; -
@tool:把普通函数包装成大模型可调用的工具; -
StateGraph:构建图; -
ToolNode:专门执行工具调用; -
tools_condition:判断模型是否请求调用工具; -
InMemorySaver:保存短期对话状态。
2. 定义 RDKit 工具
@tool
def analyze_smiles(smiles: str) -> str:
"""
Analyze a small molecule from SMILES using RDKit.
This tool calculates basic drug-likeness properties:
molecular weight, LogP, HBD, HBA, TPSA, rotatable bonds,
QED, fraction Csp3, Lipinski rule violations, and Veber rule violations.
Args:
smiles: A valid molecule SMILES string.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return json.dumps(
{
"ok": False,
"smiles": smiles,
"error": "SMILES 无法被 RDKit 正确解析。"
},
ensure_ascii=False
)
mw = Descriptors.MolWt(mol)
logp = Crippen.MolLogP(mol)
hbd = Lipinski.NumHDonors(mol)
hba = Lipinski.NumHAcceptors(mol)
tpsa = rdMolDescriptors.CalcTPSA(mol)
rot_bonds = Lipinski.NumRotatableBonds(mol)
qed = QED.qed(mol)
fraction_csp3 = rdMolDescriptors.CalcFractionCSP3(mol)
lipinski_rules = {
"MW <= 500": mw <= 500,
"LogP <= 5": logp <= 5,
"HBD <= 5": hbd <= 5,
"HBA <= 10": hba <= 10,
}
veber_rules = {
"TPSA <= 140": tpsa <= 140,
"Rotatable bonds <= 10": rot_bonds <= 10,
}
lipinski_violations = [
rule for rule, passed in lipinski_rules.items() if not passed
]
veber_violations = [
rule for rule, passed in veber_rules.items() if not passed
]
if qed >= 0.67:
qed_level = "较高"
elif qed >= 0.49:
qed_level = "中等"
else:
qed_level = "偏低"
if len(lipinski_violations) <= 1 and len(veber_violations) == 0:
judgement = "初步看具备较好的口服小分子药物性潜力"
elif len(lipinski_violations) <= 2:
judgement = "存在一定口服药物性风险,需要进一步优化"
else:
judgement = "口服小分子药物性风险较高"
result: Dict[str, Any] = {
"ok": True,
"input_smiles": smiles,
"canonical_smiles": Chem.MolToSmiles(mol),
"properties": {
"molecular_weight": round(mw, 2),
"logP": round(logp, 2),
"hbd": hbd,
"hba": hba,
"tpsa": round(tpsa, 2),
"rotatable_bonds": rot_bonds,
"qed": round(qed, 3),
"qed_level": qed_level,
"fraction_csp3": round(fraction_csp3, 3),
},
"lipinski": {
"rules": lipinski_rules,
"violations": lipinski_violations,
"num_violations": len(lipinski_violations),
},
"veber": {
"rules": veber_rules,
"violations": veber_violations,
"num_violations": len(veber_violations),
},
"judgement": judgement,
"important_note": "该结果仅用于早期药物化学与 AIDD 初筛,不代表真实活性、毒性、药效或临床可用性。"
}
return json.dumps(result, ensure_ascii=False, indent=2)
和第一版相比,这里最大的变化是加了:
@tool
这个装饰器会告诉 LangChain 和 LangGraph:这个函数不是普通函数,而是一个可以被大模型调用的工具。
3. 绑定模型和工具
MODEL_NAME = os.getenv("OLLAMA_MODEL", "llama3.1:8b")
llm = ChatOllama(
model=MODEL_NAME,
temperature=0
)
tools = [analyze_smiles]
llm_with_tools = llm.bind_tools(tools)
这里做了两件事:
第一,创建本地大模型:
llm = ChatOllama(model=MODEL_NAME, temperature=0)
第二,把工具绑定给大模型:
llm_with_tools = llm.bind_tools(tools)
绑定之后,模型在回答时就可以生成工具调用请求。
4. 编写系统提示词
SYSTEM_PROMPT = """
你是一个药物化学与 AIDD 研究 Agent。
你的任务:
1. 如果用户提供了 SMILES,并询问药物性、类药性、口服性质、ADMET 初筛、分子性质等问题,你应该调用 analyze_smiles 工具。
2. 工具返回结果后,你需要基于真实计算结果进行药学解释。
3. 不要编造计算结果。
4. 不要给临床用药建议。
5. 回答结构建议包括:
- 核心判断
- 关键指标解释
- 风险点
- 下一步研究建议
你可以处理的问题示例:
- 请分析阿司匹林的口服药物性,SMILES: CC(=O)Oc1ccccc1C(=O)O
- 这个分子是否符合 Lipinski 规则?
- 这个分子的 QED 和 TPSA 如何解读?
"""
提示词的作用不是装饰,而是给 Agent 定规矩。
它告诉模型:什么时候要调用工具,什么时候不能乱编,回答应该长什么样。
5. 定义 assistant 节点
def assistant_node(state: MessagesState):
"""
LLM 节点:
接收当前 messages,根据用户问题决定:
1. 直接回答;
2. 或发起工具调用。
"""
response = llm_with_tools.invoke(
[SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
)
return {"messages": [response]}
这个节点就是 Agent 的“大脑”。
它会读取当前对话,然后判断:
-
要不要直接回答;
-
要不要调用工具;
-
如果调用工具,要调用哪个工具。
6. 构建 LangGraph 图
def build_graph():
"""
构建最小 LangGraph Agent:
START -> assistant
assistant -> tools or END
tools -> assistant
"""
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
# tools_condition 会检查最后一条 AIMessage 是否包含 tool_calls。
# 如果包含,则进入 tools 节点;否则结束。
builder.add_conditional_edges("assistant", tools_condition)
# 工具执行完后,把工具结果交回 assistant 继续总结。
builder.add_edge("tools", "assistant")
# 加入短期记忆。相同 thread_id 会保留多轮对话状态。
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
return graph
graph = build_graph()
这段代码是 LangGraph 版本的核心。
可以按生活里的流程理解:
-
assistant像前台,先接待用户; -
tools像后厨或实验室,负责干具体的活; -
tools_condition像分诊台,判断要不要送去工具那边; -
InMemorySaver像临时笔记本,记住当前对话。
这就是有记忆 Agent 的雏形。
7. 编写运行函数
def run_agent(user_input: str, thread_id: str = "pharm-demo"):
"""
运行 Agent。
thread_id 相同,表示同一个对话线程,可以保留上下文。
"""
config = {
"configurable": {
"thread_id": thread_id
}
}
result = graph.invoke(
{
"messages": [
HumanMessage(content=user_input)
]
},
config=config
)
# 找到最后一条 AI 回复
for message in reversed(result["messages"]):
if message.type == "ai" and not getattr(message, "tool_calls", None):
return message.content
return result["messages"][-1].content
这里最重要的是:
thread_id: str = "pharm-demo"
只要 thread_id 相同,LangGraph 就会把这些对话当成同一个会话。这样,Agent 才能记住上一轮发生过什么。
如果你把 thread_id 换掉,它就像开启了一个新聊天,自然不会记得之前的内容。
8. 添加命令行入口
if __name__ == "__main__":
print("LangGraph 药学 Agent 已启动。输入 exit 退出。")
print("当前模型:", MODEL_NAME)
print("示例:请分析阿司匹林的口服药物性,SMILES: CC(=O)Oc1ccccc1C(=O)O")
print("-" * 80)
while True:
user_input = input("
你:").strip()
if user_input.lower() in ["exit", "quit", "q"]:
break
try:
answer = run_agent(user_input)
print("
Agent:")
print(answer)
except Exception as e:
print("
运行出错:")
print(type(e).__name__, str(e))
运行:
python pharm_langgraph_agent.py
测试第一轮:
请分析阿司匹林的口服药物性,SMILES: CC(=O)Oc1ccccc1C(=O)O
再测试第二轮:
第一个分子的主要风险是什么?
这时它就能根据之前的上下文继续回答。
再问:
针对这个风险有哪些优化方式?
它也能继续沿着上一轮往下说。
这就是短期记忆带来的变化。
九、两版 Agent 有什么区别?
可以用一张表说明。
| 对比项 | 最小手写版 Agent | LangGraph 版 Agent |
|---|---|---|
| 是否使用框架 | 不使用 | 使用 LangGraph |
| 工具调用方式 | 自己让模型输出 JSON,再手动解析 | 使用 bind_tools 和 ToolNode |
| 流程控制 | 自己写 if 判断 | 用图结构和条件边控制 |
| 记忆能力 | 基本没有 | 可通过 thread_id 保留短期上下文 |
| 适合阶段 | 初学,理解原理 | 进阶,构建可扩展工作流 |
| 优点 | 简单透明,容易看懂 | 结构清楚,方便扩展 |
| 缺点 | 扩展性较弱 | 初学时理解成本略高 |
如果用生活例子来说:
手写版 Agent 像你临时找一个朋友帮忙。你把问题告诉他,他判断一下,去查个工具,再回来告诉你结果。
LangGraph 版 Agent 像一个小型办事窗口。前台、分诊、工具部门、记录本都有了,流程更规范,也更容易继续扩展。
十、Agent 开发的关键不是“框架”,而是“分工”
学 Agent,最重要的不是先记住多少框架名,而是先理解分工。
在这个例子里,分工非常清楚:
用户:提出问题
LLM:理解问题、判断是否调用工具、解释结果
RDKit:负责真实计算
程序:负责把 LLM 和工具连接起来
LangGraph:负责管理流程和状态
如果分工不清,就容易出问题。
比如:
-
让大模型直接编造分子量,这是不可靠的;
-
让 RDKit 写中文研究建议,这是不合适的;
-
没有程序控制流程,工具和模型就接不上;
-
没有记忆机制,多轮对话就会断掉。
一个好的 Agent,不是让一个模型包打天下,而是让不同角色各自做自己擅长的事。
这和生活里做饭很像。
菜谱负责告诉你步骤,刀负责切菜,锅负责加热,人负责判断火候。如果你让锅去决定今天吃什么,或者让菜谱自己把菜炒熟,事情就乱了。
Agent 也是一样。
十一、初学者最容易踩的坑
1. 一上来就学大框架
很多人刚开始学 Agent,就直接看复杂框架。结果越看越迷糊。
更好的方式是:先手写一个最小 Agent,哪怕功能很简单。只要你看懂了“判断 → 工具调用 → 结果解释”这个闭环,再学框架就容易很多。
2. 没有控制模型输出格式
第一版里让模型输出 JSON,这是很重要的一步。
因为程序要解析模型输出。如果模型随便输出自然语言,程序就不知道该调用哪个工具,也不知道参数是什么。
所以规划阶段最好要求模型输出结构化内容。
3. 把工具结果和模型回答混在一起
工具负责算,模型负责解释。
比如 RDKit 算出分子量是 180.16,那么模型只能基于这个结果解释,不能自己改成 190,也不能说“这个分子一定安全有效”。
4. 忘了做异常处理
如果用户输入了错误 SMILES,程序不能直接崩溃。工具应该返回清楚的错误信息:
{
"ok": false,
"error": "SMILES 无法被 RDKit 正确解析"
}
这样大模型才能根据错误信息继续解释。
5. 以为用了 LangGraph 就一定有记忆
LangGraph 可以支持记忆,但你要正确使用 checkpointer 和 thread_id。
如果每次都换一个 thread_id,那它还是记不住。
记忆不是口号,本质上就是状态保存。
十二、下一步可以怎么扩展?
这个药学 Agent 只是一个入门例子。跑通之后,可以继续扩展。
1. 增加更多工具
现在只有一个工具:
analyze_smiles
以后可以增加:
预测 ADMET
查询 ChEMBL
分子相似性搜索
靶点信息查询
分子结构可视化
对接结果解析
这样 Agent 就不只是算几个分子性质,而是能完成更完整的 AIDD 初筛任务。
2. 增加长期记忆
现在的 InMemorySaver 更像临时记忆,程序关掉后就没了。
以后可以把重要信息保存到:
SQLite
PostgreSQL
MongoDB
向量数据库
本地 JSON 文件
比如保存用户常用的靶点、分子库、筛选标准、分析偏好。
3. 增加多步任务规划
现在的 Agent 只做简单判断。
以后可以让它完成更复杂的任务:
用户上传一批 SMILES
↓
Agent 自动批量计算性质
↓
筛选符合规则的分子
↓
生成 Excel 表格
↓
总结候选分子的优缺点
这时 Agent 就从“小工具助手”变成了“小型工作流助手”。
4. 增加人类确认环节
对于重要任务,Agent 不应该完全自动执行。
比如删除文件、提交任务、发送邮件、修改数据库,都应该让用户确认。
可靠的 Agent 不只是会做事,还要知道哪些事不能擅自做。
十三、总结:Agent 入门先抓住一条主线
Agent 入门,不要先被各种名词吓住。
先记住这条主线:
用户提出任务
↓
大模型理解任务
↓
判断是否需要工具
↓
工具执行具体操作
↓
大模型解释工具结果
↓
必要时保存上下文,支持下一轮对话
第一个项目告诉我们:不用框架,也能写出一个最小 Agent。
第二个项目告诉我们:当任务变成多轮对话和可扩展流程时,LangGraph 这样的框架会更方便。
所以,学习 Agent 最好的路径不是一开始就追求复杂,而是先跑通一个小闭环。
就像学做饭,不必第一天就做满汉全席。先把番茄炒蛋做熟,知道什么时候放油、什么时候下蛋、什么时候加盐,再谈宴席。
Agent 也是一样。
先让它正确调用一个工具,正确解释一次结果,正确记住上一轮对话。把这些小事做好,真正的 Agent 能力就开始长出来了。
更多推荐


所有评论(0)