在化学实验室担任研究科学家的时候,我的大部分工作与电影中所见的情景相去甚远:更多的是重复的常规操作,少有戏剧性动作。现实中科学家每天遇到的问题往往需要耗费大量时间和精力才能解决。生成式人工智能无法替代人类,但它能增强人类能力,加速科学发现的进程。即使未来AI技术不断发展,AI更多的是辅助而非取代科学研究中的人类。人类在科学发现中带来的创造力、直觉和跨学科洞察力依然非常重要。借助大语言模型(LLM),生成式AI应用可以帮助解释主题、解析大量科学文献、提取洞见、识别模式、生成假设,甚至执行计算和模拟等。研究人员已经使用生成式AI来撰写清晰简洁的报告、制作演示材料和撰写论文部分内容,确保研究成果准确有效地传达。
第6至第9章将介绍如何在生物学、化学、药物发现和医疗健康领域使用LangChain。本章则讨论如何构建个人助理,帮助科研和开发工作。
使用链构建助理
第3章介绍了如何在LangChain中使用链。链是一个强大概念,允许将多个操作和步骤组合成一个连贯的流程。这样的操作序列提供了一种灵活且模块化的方法,通过将复杂任务拆分为更小、更易管理的步骤来编排复杂工作,类似于人类研究者处理复杂科研任务的方式。链有助于自动化和简化科研过程中的各个环节,它允许以结构化方式将数据从一个函数或模块传递到另一个,从而完成复杂任务。
每条链可以包含多个子链或步骤,其中一个步骤的输出成为下一个步骤的输入,实现信息处理的无缝衔接。链的复杂度可以从简单的数据检索到涉及多个语言模型和外部API的复杂交互不等。一个基础的链示例可能包括将用户查询转换为API调用、读取记忆、检索数据,然后将数据格式化成用户友好的响应。
示例 5-1:使用 create_history_aware_retriever
from langchain import hub
from langchain_core.prompts import MessagesPlaceholder, ChatPromptTemplate
from langchain_core.messages import HumanMessage, AIMessage
from langchain_community.vectorstores import FAISS
from langchain.chains import create_history_aware_retriever
documents = [
Document(page_content="Experiment A: We tested the effect of fertilizer X on tomato yield at temperature 80°F."),
Document(page_content="Experiment B: We tested the effect of fertilizer X on corn yield."),
Document(page_content="Experiment C: We tested the effect of compost and different temperatures on strawberry growth."),
Document(page_content="Experiment D: Examined the small influence of compost amount on soil acidity."),
Document(page_content="Experiment E: Examined the influence of light intensity on photosynthesis in algae."),
]
vector = FAISS.from_documents(documents, embedding_model)
retriever = vector.as_retriever(search_kwargs={"k": 3})
# 用于历史感知检索的提示模板
prompt = ChatPromptTemplate.from_messages([
("system", """You help users find information about agricultural experiments. Paraphrase the user's query based on the conversation history."""),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}")
])
# 创建历史感知检索链
history_aware_retriever_chain = create_history_aware_retriever(llm, retriever, prompt)
chat_history = [
HumanMessage(content="Experiment E actually used Fertilizer X."),
AIMessage(content="Thank you for the correction. I'll note that Experiment E used Fertilizer X."),
HumanMessage(content="Experiment B had contamination issues and should be excluded from analysis."),
AIMessage(content="Noted. I'll exclude Experiment B from all analysis due to contamination issues.")
]
query = "List all experiments where fertilizer X was used with results."
> history_aware_retriever_chain.invoke({"chat_history": chat_history, "input": query})
# AI生成内容:
> Paraphrased query: Please provide a list of all experiments that used Fertilizer X. Make sure to include Experiment E and exclude Experiment B from this list.
>>> Experiment B, Experiment A, Experiment D
如示例5-1所示,尽管系统正确地对查询进行了意图重述,但结果并不正确,因为实验B(应排除)被检索出来了,而实验E(明确指出使用肥料X)未被检索到。这是因为嵌入模型通过测量用户问题与LLM生成的重述版本的相似度来判断相关内容,而重述版本只是作为上下文参考。相比之下,chat_history在LLM链中使用效果更佳。
一个更好的例子是查询使用肥料Y的实验列表,在本案例中肥料Y即为堆肥。由于实验中没有直接出现“肥料Y”这一术语,单纯检索很可能导致错误结果。但若结合chat_history,其中说明肥料Y等同于堆肥,LLM在重述查询时会考虑此信息,检索链将返回实验C和D:
query = "List all experiments where Fertilizer Y was used"
# 不使用聊天历史作为基线
> history_aware_retriever_chain.invoke({"chat_history": [], "input": query})
# AI生成内容:
> Paraphrased query: Can you provide a list of all the experiments that utilized Fertilizer Y?
>>> Experiment B, Experiment A, Experiment D
# 加入聊天历史进行重述
chat_history = [
HumanMessage(content="Was Fertilizer Y used in the experiments?"),
AIMessage(content="No, Fertilizer Y wasn't used in any of the experiments."),
HumanMessage(content="That's incorrect. Fertilizer Y and compost are the same."),
AIMessage(content="Thank you for the correction. I'll note that Fertilizer Y and compost are the same."),
]
> history_aware_retriever_chain.invoke({"chat_history": chat_history, "input": query})
# AI生成内容:
> Paraphrased query: Provide details and results of all experiments where Fertilizer Y (compost) was used.
>>> Experiment D, Experiment C, Experiment A
create_history_aware_retriever只是LangChain支持的链类型之一。这些导入都是基于LCEL链的封装。如果你查看create_history_aware_retriever、stuff_documents_chain和create_sql_query_chain的源码(示例5-2),你会发现链的构造遵循特定的序列。
示例 5-2:LCEL链的源码示例
# create_history_aware_retriever
return (
RunnableBranch(
(
lambda x: not x.get("chat_history", False), # 如果没有聊天历史
(lambda x: x["input"]) | retriever, # 直接传递输入给检索器
),
prompt
| llm
| StrOutputParser()
| retriever,
).with_config(run_name="chat_retriever_chain")
)
# stuff_documents_chain
return (
RunnablePassthrough.assign(**{DOCUMENTS_KEY: format_docs}).with_config(run_name="format_inputs")
| prompt
| llm
| StrOutputParser()
).with_config(run_name="stuff_documents_chain")
# create_sql_query_chain
return (
RunnablePassthrough.assign(**inputs) # type: ignore
| (
lambda x: {k: v for k, v in x.items() if k not in ("question", "table_names_to_use")}
)
| prompt_to_use.partial(top_k=str(k))
| llm.bind(stop=["\nSQLResult:"])
| StrOutputParser()
| _strip
)
上述LCEL链遵循特定的重复模式:
- 上下文(Context)
- 提示(Prompt)
- 语言模型(LLM)
- 输出解析器(Output parser)
为了更好地理解链式助理的工作原理,我们来仔细看看 LCEL 链是如何构建的。最简单的链可以仅由一个环节组成,这个重要的环节就是 LLM(如 ChatAnthropic、ChatOpenAI 或其他聊天模型)。例如,我们向模型提问:“什么是债券?”:
chain_1 = llm
> chain_1.invoke(("human", "What is a bond?"))
>>> AIMessage(content='A bond is a fixed income investment in which an investor loans money to an entity (such as a corporation or government) for a defined period of time at a fixed interest rate. The entity borrows the ...',
response_metadata={'token_usage': {'completion_tokens': 92, 'prompt_tokens': 17, 'total_tokens': 109}, ...
这个回答在技术上是正确的,但化学家会期待完全不同的答案。这也是为什么在 LCEL 链中通常会先用提示指令来引导模型。链的输入是一个包含“question”键及其对应值(如“What is a bond?”)的字典,提示模板会利用这个输入构建一个针对语言模型的专属提示。例如,下面的示例用 ChatPromptTemplate 将查询修改为:“你是一名化学家,请回答以下问题:什么是键?”在很多情况下,“你是一名化学家”这类短语被用作系统提示:
prompt = ChatPromptTemplate.from_template("""
You are a chemist. Answer the following question: {question}
""")
chain_2 = prompt | model
> chain_2.invoke({"question": "What is a bond?"})
>>> AIMessage(content='In chemistry, a bond is a strong force of attraction that holds atoms together in a molecule. Bonds are formed when atoms share or ...', response_metadata={'token_usage': {'completion_tokens': 81, 'prompt_tokens': 23, 'total_tokens': 104}, ...
注意
管道操作符 | 允许将函数串联起来,将前一个函数的输出作为下一个函数的输入。当 Python 解释器看到 a | b,它会尝试调用对象 a 的 __or__ 方法,参数为 b。这使得可以定义自定义的 __or__ 方法,将两个对象组合成一个新的可调用对象。Runnable 类就演示了这一点,它封装了一个函数并定义了 __or__ 方法,返回一个新的 Runnable 实例,其 __call__ 方法按顺序调用原始的两个函数。这样,就能使用管道操作符把函数链起来,每个函数都消耗前一个的输出。
加入提示后,结果更符合预期,但输出中包含了元数据和一些用户不需要的细节。输出解析器的作用就是将语言模型(BaseMessage 对象)的输出转成期望的格式。StrOutputParser 简单地将消息内容转为普通的 Python 字符串,这个字符串即为链的最终输出,可以直接使用或展示。也可以用其他输出解析器来实现不同的格式化,比如提取特定信息或转成其他数据结构:
output_parser = StrOutputParser()
chain_3 = prompt | model | output_parser
> chain_3.invoke({"term": "What is a bond?"})
>>> In chemistry, a bond is a force that holds two or more atoms together in a molecule. Bonds are formed when electrons are shared, transferred, or ...
现在,我们得到了可以传给用户的最终输出。
到目前为止示例中的链也包含了 Runnable 组件。Runnable 是 LangChain 中的关键概念,它支持创建模块化、可组合的链或管道,将不同组件组合起来。Runnable 与 LangChain 深度集成,使得将提示、模型、输出解析器等组件灵活地整合到管道中变得轻松。主要的 Runnable 类型有:
RunnableParallel:允许多个组件并行执行并合并它们的输出RunnableLambda:包装一个 Python 函数,使其兼容管道操作RunnableGenerator:用于需要流式处理的复杂转换RunnableBranch:根据条件选择执行哪个分支RunnableEachBase:对每个输入元素调用另一个RunnableRunnablePassthrough:原样传递输入,不做改动
组合这些 Runnable,可以构建复杂的链条,实现数据检索、提示生成、模型执行、后处理以及自定义逻辑的一体化流程。
第4章讨论了 RAG 系统如何提供上下文相关的结果。合理的做法是将检索作为 prompt | model | output_parser 链的第一步,这样就能构建上下文并传递给 LLM,示例5-3展示了这种 RAG 的 LCEL 管道示例。
vectorstore = DocArrayInMemorySearch.from_texts(
[
"DNA carries genetic information within cell chromosomes.",
"Ecosystems consist of living organisms and their physical environment.",
"Some bacteria cause diseases while others are beneficial.",
"Homeostasis maintains steady internal conditions in living systems.",
"Natural selection helps organisms adapt and survive in their environments.",
"Mitochondria produce ATP, the main energy source for cells.",
"Photosynthesis in plants produces oxygen.",
"The brain controls body functions and is located in the skull.",
"The immune system defends against harmful substances by detecting antigens."
],
embedding=embedding_model,
)
retriever = vectorstore.as_retriever()
template = """Answer the question based only on the following context: {context}
Question: {question}"""
prompt = ChatPromptTemplate.from_template(template)
output_parser = StrOutputParser()
retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = retrieval | prompt | model | output_parser
> chain.invoke("How do plants release oxygen?")
>>> Plants release oxygen through photosynthesis.
> chain.invoke("What is the main energy source for cells?")
>>> The main energy source for cells is ATP, which is produced by mitochondria.
> chain.invoke("Who painted mona Lisa?")
>>> I'm sorry, I cannot provide an answer to that question based on the given context.
至此,我们对 LCEL 语法有了更深入的了解。你可能会想,为什么不直接用纯 Python 来实现类似逻辑?Runnable 和 LCEL 是在 LangChain 之上添加的一层抽象,虽然它们并不完全符合 Python 社区的 PEP20 和 Python之禅等流行标准,并且语法上也不太“Pythonic”。此外,Runnable 旨在高效运行,但 LCEL 由于其动态特性,在计算密集型任务中可能带来性能开销。工具支持和调试能力有限,且 LCEL 需要将中间输入和最终输出从链的起点传递到终点,导致大部分链需要额外参数,即便这些参数未被使用。
然而,在工业级应用中使用 LCEL 有显著的优势,比如利用 Runnables 将复杂的流水线拆分成更小、更可复用的组件。这使你能够使用 RunnableParallel 并行运行多个组件,在处理独立任务或数据源时显著提升性能。LCEL 还内置对异步操作的支持,并为所有链提供一致的接口,使得工业化你的代码更加容易,支持开箱即用的流式处理、异步执行、降级模型、类型注解、运行时配置等功能。
Runnables 还提供了一种简单机制来链接多个链,从而设计和创建更复杂的流水线。这大大增强了应用的灵活性和可扩展性,使得在 LangChain 框架内处理复杂的数据转换和交互变得更加容易。下面的示例(示例 5-4)演示了如何通过传递参数来串联两个不同的链。我们将传入症状信息,期望得到可能的实验室检查结果。
示例 5-4. 多个 LCEL 链
# 根据症状判断疾病的提示
prompt_symptom = ChatPromptTemplate.from_template(
"Based on these symptoms, what disease might this person have: {symptoms}?"
)
# 根据疑似疾病推荐实验室检查的提示
prompt_disease = ChatPromptTemplate.from_template(
"What lab exams should be taken to confirm a diagnosis of {disease}?"
)
# 用于根据症状确定疾病的子链
sub_chain_symptom = prompt_symptom | llm | StrOutputParser()
sub_chain_disease = prompt_disease | llm | StrOutputParser()
# 主链,将第一个子链的输出作为字典传给第二个子链
main_chain = (
{"disease": sub_chain_symptom}
| sub_chain_disease
)
> main_chain.invoke({"symptoms": "fever, cough, and shortness of breath"})
>>> Recommended Lab Exams: Some lab exams that may be helpful in confirming a
diagnosis of pneumonia, bronchitis, or COVID-19 include:
1. Chest X-ray: This imaging test can help identify any abnormalities in the
lungs, such as inflammation or fluid buildup, which may indicate pneumonia.
2. Complete blood count (CBC): This test can help determine if there is an
infection present by measuring the levels of white blood cells, red blood cells,
and platelets in the blood.
3. C-reactive protein (CRP) test: Elevated levels of CRP in the blood may
indicate inflammation, which can be a sign of pneumonia or other infections.
...
示例 5-4 中包含了三条链:一条主链和两条子链。每条子链都遵循相同的 prompt | model | output_parser 结构。主链负责将 sub_chain_symptom 的输出作为字典输入传递给 sub_chain_disease。图 5-1 展示了我们将在后续示例中开发的链条结构的可视化。
示例 5-5 是一个更复杂的多个子链合并示例。我们来看一个场景:在特定条件下的观察结果可用于实验设计并预测实验结果。系统从观察中生成假设,设计实验来验证这些假设,并预测实验可能的结果。随后生成预期结果,展示实验和假设推理过程。这种方法对于需要处理大量数据并高效生成可测试假设的领域尤为重要。链式架构允许自动化推理,模拟科学家的思考过程,潜在地加快发现速度。
提示
在 Python 中调用 chain.get_graph() 并配合 print_ascii()、draw_mermaid()、draw_png() 或 draw_mermaid_png() 等方法,可以实现对已创建链的可视化。
示例 5-5. 使用 LCEL 链进行实验设计
prompt_observation = ChatPromptTemplate.from_template(
"""Based on the observation: {observation}, what is a possible biological
explanation or hypothesis?"""
)
prompt_hypothesis = ChatPromptTemplate.from_template(
"""What experiment could we perform to test the hypothesis: {hypothesis},
considering the condition: {condition}?"""
)
prompt_experiment = ChatPromptTemplate.from_template(
"""Given the setup: {experiment_setup}, what might be the expected outcome
of this experiment?"""
)
hypothesis_generator = (
{"observation": RunnablePassthrough()}
| prompt_observation | llm | StrOutputParser()
)
experiment_suggestion = RunnableParallel(
{"hypothesis": hypothesis_generator, "condition": RunnablePassthrough()}
) | prompt_hypothesis | llm | StrOutputParser()
experiment_outcome = (
{"experiment_setup": experiment_suggestion}
| prompt_experiment | llm | StrOutputParser()
)
> experiment_outcome.invoke(
{"observation": "Pea plants with round seeds produce mostly round seed
offspring, even when crossed with wrinkled seeds.",
"condition": "controlled pollination"
}
)
>>> Experiment Prediction: The expected outcome of this experiment would be that the
majority of the offspring plants will have round seeds, even if one parent has
wrinkled seeds. This would support the hypothesis that the trait for seed shape
in pea plants is controlled by a single gene with two alleles, one for round
seeds (dominant) and one for wrinkled seeds (recessive). The expected ratio of
round seeds to wrinkled seeds in the offspring would be approximately 3:1, as
predicted by Mendel's laws of inheritance for a dominant-recessive gene pair.
示例 5-6 展示了一个辩论原型。我们将用 RunnableParallel 来实现处理链的分支:一个组件的输出同时传递给多个后续组件,后续组件再合并结果,综合生成最终答复。
提示
注意我为每个子链定义了模型和输出解析器。在必要时切换到特定领域的微调模型非常有意义。如果后续输出需要特定格式(字符串、字典、pandas dataframe 等),更换输出解析器也很有用。
我们将设置所有链,确保一旦生成论点,arguments_for 和 arguments_against 两个链都会被调用,执行结果传递给 final_responder 链。主链 main_chain 用于协调整个流程,类似示例 5-4。
示例 5-6. 使用 LCEL 链构建辩论原型
# 生成初始论点的链
generate_argument = (
ChatPromptTemplate.from_template(
"Generate an argument about: {input}"
)| llm | StrOutputParser() | {"base_response": RunnablePassthrough()}
)
# 列出论点优点的链
arguments_for = (
ChatPromptTemplate.from_template(
"List the pros or positive aspects of: {base_response}"
)| llm | StrOutputParser()
)
# 列出论点缺点的链
arguments_against = (
ChatPromptTemplate.from_template(
"List the cons or negative aspects of: {base_response}"
)| llm | StrOutputParser()
)
# 综合辩论的最终答复链
final_responder = (
ChatPromptTemplate.from_template(
"""Discussion on {input}:
Pros: {arguments_for}
Cons: {arguments_against}
Can you provide a balanced conclusion?"""
)| llm | StrOutputParser()
)
# 管理辩论流程的完整主链
main_chain = (
generate_argument
|{
"arguments_for": arguments_for,
"arguments_against": arguments_against,
"input": itemgetter("base_response") # 获取 base_response
}
| final_responder
)
> chain.invoke({"input": "the use of CRISPR technology in human embryos"})
>>> Debate Summary: In conclusion, the use of CRISPR technology in human embryos
presents both potential benefits and significant ethical and societal concerns.
While proponents argue that it has the potential to eliminate genetic diseases,
reduce suffering, and enhance desirable traits, opponents raise valid concerns
about ethical implications, unintended consequences, unequal access, and lack
of regulation...
利用 Runnables,你可以创建路由链并在链中实现循环。如果结合 RunnableBranch 和以 RunnableLambda 作为循环结束语句,可以实现复杂逻辑。因篇幅限制,本章不做详述,但这是练习 LangChain 技能的极好机会。
你正逐步接近链的高手。接下来我们要讲的是代理(agents)。
构建基于代理的助手
生成式 AI 可以通过设置针对特定任务的链(前面讨论过)或代理(第3章)来自动化和简化许多日常流程。虽然链和代理看似相似,但两者有一个核心区别。链是一系列预设的步骤(虽然可以包含分支),而代理则是一个组件,它可以使用附加工具并自主定义步骤顺序。示例5-7构建了一个简单的计算器代理,演示代理的可能性。我们将为数学运算指定多个工具,让代理根据逻辑和正确的运算顺序(PEMDAS:括号、指数、乘除、加减)决定使用哪些工具以及何时使用。
示例5-7. 计算器代理
class TwoNumericInputs(BaseModel):
a: float = Field(description="第一个数字")
b: float = Field(description="第二个数字")
# @tool(工具名称, 参数的schema)
@tool("sum_2_numbers", args_schema=TwoNumericInputs)
def sum_2_numbers(a, b):
"""两个数字相加。"""
return a + b
@tool("substract_2_numbers", args_schema=TwoNumericInputs)
def substract_two_numbers(a, b):
"""两个数字相减。"""
return a - b
@tool("multiply_2_numbers", args_schema=TwoNumericInputs)
def multiply_two_numbers(a, b):
"""两个数字相乘。"""
return a * b
@tool("divide_2_numbers", args_schema=TwoNumericInputs)
def divide_two_numbers(a, b):
"""两个数字相除。"""
return a / b
tools = [sum_2_numbers, substract_two_numbers, multiply_two_numbers, divide_two_numbers]
prompt = ChatPromptTemplate.from_messages(
[
("system",
"你是一个计算器。请遵循PEMDAS规则。确保使用这些工具。如果工具无法使用,请尽力完成运算。",
),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
]
)
calculator_agent = create_tool_calling_agent(llm, tools, prompt)
calculator_agent_executor = AgentExecutor(
agent=calculator_agent, tools=tools, verbose=True, handle_parsing_errors=True
)
请注意,calculator_agent 并不受限于固定的运算顺序。当收到查询时,代理会分析文本提取数字值,并判断请求的数学运算类型,无论是明确说明(比如“加5和3”),还是通过数学符号隐含表达(如5 + 3)。代理会将该运算匹配到其可用的相应工具(例如sum_2_numbers、substract_2_numbers、multiply_2_numbers、divide_2_numbers),确保对提取的值应用正确的数学函数。
一旦确定了合适的工具,代理会根据 TwoNumericInputs 的 schema 验证输入参数,确认两个值均为有效的浮点数。代理在计算含多个运算的表达式时遵循PEMDAS规则,将复杂计算拆分为多个按顺序调用的工具。例如,计算“3 + 4 * 2”时,代理会先对4和2调用multiply_2_numbers,再用sum_2_numbers将3与结果相加,而非简单从左到右依次计算。
如果输入中包含未由可用工具直接支持的运算(如乘方或开方),代理会根据提示中的“如果工具不能用,请尽力完成运算”回退使用其内置能力。此综合方案使计算器代理能处理广泛的数学问题,同时保持准确性并对计算过程保持透明。
下面的代码演示了 calculator_agent_executor 如何执行正确的数学操作:
# 继续 <<ch5_calculator_agent>>
> calculator_agent_executor.invoke({"input": "3 / 3 + 3 * 3"})
#-------------------------AI生成内容开始-------------------------
> 进入新的 AgentExecutor 链...
调用: `multiply_2_numbers` 参数 `{'a': 3, 'b': 3}`
9.0
调用: `divide_2_numbers` 参数 `{'a': 3, 'b': 3}`
1.0
调用: `sum_2_numbers` 参数 `{'a': 1, 'b': 9}`
10.0
表达式 3 / 3 + 3 * 3 的结果是 10。
> 链执行结束。
#-------------------------AI生成内容结束---------------------------
>>> {'input': '3 / 3 + 3 * 3',
'output': '表达式 3 / 3 + 3 * 3 的结果是 10.'}
> calculator_agent_executor.invoke({"input": "将上一个结果除以二再减去5"})
#-------------------------AI生成内容开始-------------------------
> 进入新的 AgentExecutor 链...
调用: `divide_2_numbers` 参数 `{'a': 10, 'b': 2}`
5.0
调用: `substract_2_numbers` 参数 `{'a': 5, 'b': 5}`
0.0
上一个结果的一半(5)减去5等于0。
> 链执行结束。
#-------------------------AI生成内容结束---------------------------
>>> {'input': '将上一个结果除以二再减去5',
'output': '上一个结果的一半(5)减去5等于0。'}
我们在示例5-7中使用 create_tool_calling_agent 的原因是它支持调用接受多个输入的工具。工具调用允许模型检测何时应调用一个或多个工具,并响应应传递给这些工具的输入。示例5-7、5-8和5-9中可调用的工具展示于图5-2。
代理的独立性和丰富的工具支持,使得它们比链条使用起来更加灵活。例如,可以设计一个代理来执行全面的文献检索、总结关键发现,或者生成初步的文献综述草稿。不同的代理可以负责清理和预处理数据,确保数据一致性并遵循既定标准,从而让研究人员专注于数据分析和解读。此外,代理还能被配置来处理各种写作任务,比如生成研究提案、报告草稿,甚至部分科学论文(这类操作的缺点在第1章中讨论过)。利用生成式AI的自然语言生成能力,这些代理能够产出高质量、连贯的文本,同时遵守特定的格式规范和引用风格,节省时间并确保研究成果的表达一致且准确。
示例5-8和5-9展示了一个科学文献检索代理,能够查询Scopus和arXiv数据库。为方便讲解,设置和执行代理的代码分为几部分,完整代码可见LangChain4LifeSciencesHealthcare仓库。Scopus的API密钥可在其开发者门户申请。
示例5-8. 科学文献检索代理设置
generate_url_prompt_template = """
根据用户请求生成SCOPUS API查询URL。
说明:
- 使用布尔运算符(AND,OR,AND NOT)组合关键词。
- 对查询字符串进行URL编码。
...
(完整提示模板见官方GitHub仓库:
https://github.com/IvanReznikov/LangChain4LifeSciencesHealthcare)
...
用户请求:"list articles with chatgpt keyword in french"
URL查询:https://api.elsevier.com/content/search/scopus?query=key(chatgpt)
%20AND%20Language(french)&apiKey={{API_KEY}}&view=STANDARD&
httpAccept=application%2Fjson
用户请求:{user_query}
URL查询:
"""
generate_url_prompt = PromptTemplate.from_template(generate_url_prompt_template)
chain = (
{"user_query": RunnablePassthrough()}
| generate_url_prompt | llm | StrOutputParser()
)
generate_scopus_query_tool = Tool(
name="generate_scopus_url", func=chain.invoke,
description="生成Scopus查询URL的工具"
)
@tool("request_url")
def request_url(url: str) -> list:
"""调用给定Scopus API URL并返回解析后的条目列表。"""
headers = {...} # 需自行定义或传入
response = requests.get(url, headers=headers)
response.raise_for_status() # 捕获异常
return response.json().get("search-results", {}).get("entry", [])
代理提示(agent_prompt)从LangChain Hub拉取并做了适当修改:
agent_prompt = hub.pull("hwchase17/react")
agent_prompt.template = """
你是科学文献检索助手。你可以访问scopus和arxiv。
查询Scopus时,先使用generate_scopus_url生成查询URL,
再用query_url工具执行。
如果提到arxiv或scopus,则查询对应库,否则同时查询。
以如下格式提供最终结果:
Arxiv结果:<arxiv结果>
Scopus结果:<scopus结果>
""" + agent_prompt.template
tools = load_tools(["arxiv"]) + [generate_scopus_query_tool, request_url]
sci_search_agent = create_react_agent(llm, tools, agent_prompt)
agent_executor = AgentExecutor(
agent=sci_search_agent, tools=tools, verbose=True, handle_parsing_errors=True
)
示例5-8中工具定义方式有三种:
load_tools:用于加载arXiv工具(from langchain.agents import load_tools)Tool:用于Scopus工具(from langchain_core.tools import Tool)@tool装饰器:用于URL请求工具(from langchain_core.tools import tool)
arXiv工具已内置LangChain,通过load_tools加载。Scopus暂无现成工具,因此我们基于公共API自定义开发。根据文档,Scopus搜索结果可通过构造的链接访问,本示例中实现了两个工具:一个生成URL,一个调用API获取数据。
小贴士:借助LLM的现代编程能力,你可以考虑用LLM直接设计生成Scopus URL的算法函数,取代少量示例法。这样更可靠且节省token,因为提示词更短。
根据Scopus API文档,构造URL时可以添加几个示例以辅助few-shot训练。用链条将所有逻辑封装起来,再包装成工具供代理调用是理想方案。生成Scopus链接后,使用request_url工具调用API获取数据。
代理提示采用标准ReAct模板(来自LangChain Hub),稍作改动以涵盖代理任务,指示何时用何工具及结果格式。
示例5-9展示了执行“列出2024年底发表的5篇关于LangChain的论文”的结果。你可以看到Reasoning-Action框架的工作流程,代理依次进行“思考→动作→动作输入→观察”,下一个思考依赖于当前问题或上一步观察,最终输出答案。
示例5-9. 科学文献检索代理执行
> agent_executor.invoke({"input": "List 5 papers dedicated to langchain
published at the end of 2024" })
> 进入新的AgentExecutor链...
我需要搜索2024年底发表的关于“langchain”的论文。
由于未指定具体来源,我将同时查询Arxiv和Scopus。
动作:arxiv
动作输入:langchain 2024
发布:2024-02-21
标题:通过LangChain革新心理健康护理:大型语言模型的历程
...
(另外2篇论文)
...
我找到了Arxiv上关于“langchain”的一些论文,现在将查询Scopus中2024年底发表的其他论文。
动作:generate_scopus_url
动作输入:"langchain" AND PUBYEAR IS 2024
根据用户请求 "langchain" AND PUBYEAR IS 2024,生成SCOPUS API查询URL,搜索关键词“langchain”,筛选发表年份为2024年。由于用户未指定结果数量或排序,默认返回10条,按最新发表排序。
构造的URL是:
https://api.elsevier.com/content/search/scopus?query=TITLE-ABS-KEY(langchain)&
date=2024&sort=-coverDate&count=10...
该URL在标题、摘要和关键词中搜索“langchain”,筛选2024年发表,按最新发表日期排序,限制结果为10条。已生成Scopus查询URL,接下来调用此URL获取论文数据。
动作:request_url
动作输入:https://api.elsevier.com/content/search/scopus?query=TITLE-ABS-KEY
(langchain)&date=2024&sort=-coverDate&count=10...
...
"dc:title": "破解集成LLM应用中的RCE漏洞",
"dc:creator": "刘涛",
"prism:publicationName": "CCS 2024 - 2024年ACM SIGSAC计算机与通信安全会议论文集",
"prism:coverDate": "2024-12-09",
...
(另外9篇论文)
...
我已收集Arxiv和Scopus中关于“langchain”的相关论文。结果如下:
最终答案:以下是一些2024年底发表的关于“langchain”的论文。
Arxiv结果:
1. **标题:** 通过LangChain革新心理健康护理:大型语言模型的历程
- **作者:** Aditi Singh, Abul Ehtesham, Saifuddin Mahmud, Jong-Hoon Kim
- **发表时间:** 2024-02-21
- **摘要:** 论文讨论了LangChain在创建心理健康辅助聊天机器人MindGuide中的应用。
(另外2篇论文)
Scopus结果:
1. **标题:** 破解集成LLM应用中的RCE漏洞
- **作者:** 刘涛
- **发表时间:** 2024-12-09
- **会议:** CCS 2024 - 2024年ACM SIGSAC计算机与通信安全会议论文集
(另外4篇论文)
这些论文介绍了LangChain及大型语言模型的多种应用与研究。
> 链执行结束。
你可能会问:为什么要自己开发工具、链条、代理和应用?答案很简单:信任与控制。我见过多款宣称实时搜索的科学检索应用,实际上只是查询了自己的数据库或向量存储,而这些数据可能严重过时。另一个常见问题是使用原生arXiv工具时出现“No good arxiv result was found”响应,往往是查询不正确导致的。
此外,一旦你掌握了 LangChain,你就可以随时定制你的生成式 AI 应用。在示例5-9中,你可能注意到 arXiv 的结果发布时间比 Scopus 的要早。其主要原因如示例5-10所示,是因为默认的 arXiv LangChain 工具不支持排序选项,而 arXiv 的 API 以及作为该工具基础的原生 Python 包都支持排序功能。
示例5-10. LangChain 封装与原生 arXiv 包的比较
import arxiv
from langchain_community.utilities.arxiv import ArxivAPIWrapper
# 检索包含'langchain'且提交时间在2024年的arxiv论文
query = 'langchain AND submittedDate:[20240101000000 TO 20241231235959]'
# LangChain 封装的arxiv工具
arxiv_tool = ArxivAPIWrapper(
top_k_results=3,
# ...
)
results = arxiv_tool.run(query)
#-------------------------AI生成内容-------------------------
# 示例输出
Published: 2024-06-26
Title: Poisoned LangChain: Jailbreak LLMs by LangChain ...
Published: 2024-02-21
Title: Revolutionizing Mental Health Care through LangChain: ...
Published: 2024-12-13
Title: Enhancing Nursing and Elderly Care with Large Language Models: ...
#-------------------------结束-------------------------
client = arxiv.Client()
search = arxiv.Search(
query, max_results=3,
sort_by=arxiv.SortCriterion.SubmittedDate,
sort_order=arxiv.SortOrder.Descending
)
results = list(client.results(search))
for result in results:
print(f"Title: {result.title}")
print(f"Date: {result.published}")
# 预期输出示例
# Title: CypherBench: Towards Precise Retrieval over Full-scale Modern ...
# Date: 2024-12-24 23:22:04+00:00
# Title: Enhancing Nursing and Elderly Care with Large Language Models: ...
# Date: 2024-12-13 08:10:56+00:00
# Title: Agent AI with LangGraph: A Modular Framework for Enhancing Machine ...
# Date: 2024-12-05 01:45:12+00:00
语言模型的写作能力常被视为生成式AI最强大的应用之一,尤其是语言模型本身训练于大量文本数据。然而,超越这些基础优势,真正的广阔前景在于协调多代理团队共同应对更复杂的研究挑战。单个代理虽然适合处理特定的例行任务,但生成式AI的真正威力在于组织多个拥有自己提示、模型和工具的代理组成团队。通过整合多个专门代理的能力,研究人员能够创建强大的工作流,顺畅整合研究过程中的各个环节。
举例来说,假设一个项目目标是为某种疾病筛选潜在药物候选分子。可组建一支代理团队,每个代理负责过程中的一项工作。一个代理负责全面文献回顾,筛选相关研究并提炼关键结论;另一个代理分析分子结构和生物通路数据库,利用机器学习模型预测药物-靶标相互作用;还有一个代理设计并模拟虚拟实验,评估潜在候选药物的效果与安全性。
这些代理可互相协作,分享各自的输出和见解。一个协调代理监督整个流程,确保各代理的结果整合并进行统一分析。这种协同方式充分发挥各代理的优势,同时弥补单个代理的局限,形成更全面、高效的研究管道。此外,代理团队的模块化特性赋予灵活性和适应性。随着新研究挑战出现或优先级调整,团队组成可重新配置,增添新代理或调整现有代理以满足项目变化需求。这种敏捷性将加速研究进程,促进更具活力和响应性的科学生态系统。
使用多代理构建助手
LangChain 的核心一直聚焦于有向链(directed chains),这也体现在其名称中(不过 LangChain 也在大量投入图结构的研发,本节会涉及相关内容)。任务通常以线性序列或无环有向图(DAG)的形式组织,每一步按照预定顺序执行,保证操作流程清晰明了。这种结构非常适合数据流水线或工作流自动化等场景。LangChain 在任务必须严格依赖前一步完成后才能执行的流程中表现出色,符合无环有向图的设计原则。
然而,如果我们想要构建一个能彼此交流的代理团队,就需要另一种代理组织方式。LangGraph 是 LangChain 的一个产品,章节3中有简要介绍,它是一种更灵活的基于图的结构,支持创建带有循环的工作流,使任务能够反复往返执行。这种从线性链到复杂图的演变,使得系统在处理动态和迭代过程时更具适应性和复杂性。LangGraph 允许在代理运行时引入循环,这一点尤为重要,因为许多任务需要迭代改进,如搜索查询优化或持续学习系统。
注:章节10 会介绍其他流行的替代框架,如 CrewAI 和 AutoGen。
在 LangChain 中,代理是由语言模型驱动的系统,负责决定执行哪些操作。代理运行时在循环中运行代理,决定动作、执行、记录观察结果,直到代理决定停止。LangGraph 在此基础上进行改进,使得定制代理运行时更简单灵活,相较于之前的 AgentExecutor 类(负责工具调用和错误处理),提供了更动态的运行时创建方式。
LangGraph 的一大创新是引入了循环代理运行时,解决了传统框架(如 LCEL)中缺乏循环执行代理的限制。LangGraph 拥有类似 LangChain 的 AgentExecutor 类,还有一个 ChatAgentExecutor,它将代理状态管理为消息列表,这对于基于聊天的模型尤为有用,因为函数调用和响应都以消息形式存在。LangGraph 允许对这些基础执行器进行修改,比如添加人工干预(human-in-the-loop)交互,或指定工具的使用顺序,从而增强代理执行的灵活性和功能性。
在 LangGraph 中,代理的运行时通过图状态(graph state)跟踪,每个节点可以向状态推送更新,避免了从节点到节点传递完整状态的复杂性。代理状态和节点被定义为支持代理做决策和执行工具。状态包括输入、聊天历史、代理结果以及代理采取的步骤列表。节点则定义代理的动作和工具执行逻辑,节点之间由边连接,构成条件或连续的工作流。条件边允许根据代理的不同结果走不同路径,而普通边确保代理可以持续返回进行后续操作。这样的设计支持动态且灵活的代理运行时。
示例5-11实现了一个包含两个节点及两种类型边的代理工作流。LangGraph 简单代理架构的图示见图5-3。
LangGraph 还引入了将图结构编译成可执行形式的概念,使其能够处理输入并产生输出。通过带有特定输入的图运行,代理能够做出决策、调用工具并相应地更新其状态。每个节点的结果以及最终的代理状态都可以被观察,提供对代理执行过程的全面概览。这种方法便于代理功能的定制和扩展,使 LangGraph 成为开发高级基于代理系统的强大工具。示例 5-11 展示了一个包含多次循环、用于回答查询的单一 LangGraph 代理实现。
LangGraph 代理的核心架构围绕状态机展开,我们的案例中主要包含两个组件:代理节点和工具节点(见示例 5-11)。代理节点使用语言模型处理推理和回答生成,工具节点执行专门的功能以查询 PubMed 和 Semantic Scholar 数据库。这些节点通过有向图结构通信,条件路由决定是否需要调用工具或直接由代理回应。系统通过记忆检查点维护会话状态,实现多轮持久交互。
示例 5-11. LangGraph 代理
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
class State(TypedDict):
messages: Annotated[list, add_messages]
tools = [PubmedQueryRun(), SemanticScholarQueryRun()]
tool_node = ToolNode(tools=tools)
llm_with_tools = llm.bind_tools(tools)
def run_agent(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
graph_builder = StateGraph(State)
graph_builder.add_node("agent", run_agent)
graph_builder.add_node("tools", tool_node)
graph_builder.set_entry_point("agent")
graph_builder.add_conditional_edges("agent", tools_condition)
graph_builder.add_edge("tools", "agent")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
user_input = """
Determine the precise genomic locations and the degree of change in DNA
methylation patterns in Arabidopsis thaliana after severe drought stress.
After finding the specified genomic locations and numerical methylation
changes list specific genes that exhibit significant changes in
expression. Using the specified genes list, quantify the resulting
levels of abscisic acid (ABA) in the plant tissues."
"""
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
{"configurable": {"thread_id": "1"}}, stream_mode="values",
)
该实现最强大的方面是它能执行多步骤的科学研究任务,类似于许多 AI 公司所称的“深度研究”。语言模型配置为在必要时调用工具,形成信息反馈循环,从学术数据库检索的信息用于后续推理。这使代理能够处理复杂的研究问题,需要多步信息收集并综合不同来源的发现。图结构(见图 5-4)管理思考(代理节点)和信息检索(工具节点)之间的控制流,支持将数据库查询和分析步骤串联,逐步构建全面的答案。示例 5-12 展示了该实现的结果。
示例 5-12. LangGraph 代理续篇
for event in events:
event["messages"][-1].pretty_print()
输出内容示例:
AI Message ==================================
Tool Calls: pub_med (call_zUeM3EJCBhFh5WzyzrOcQOCb)
Args: query: Arabidopsis thaliana DNA methylation patterns drought stress
Tool Message =================================
Name: pub_med
Published: 2019-08-24
Title: Genome-wide identification, classification, expression profiling and DNA
methylation (5mC) analysis of stress-responsive ZFP transcription factors in
rice (Oryza sativa L.).
... (更多出版数据)
(后续类似查询和返回略)
总结部分示例输出:
- 基因组位置和甲基化变化:在干旱胁迫下,关键应激响应基因(如 DREB1、LEA 等)显示甲基化水平变化。
- 表达显著变化的基因:如 DREB1、LEA、SOS1、RD29A、ABF3 和 RD26 在干旱胁迫中表现出显著的表达变化。
- ABA 含量的定量:这些应激相关基因的上调伴随着拟南芥组织中 ABA 浓度的升高。
从代码中可以看到,代理有条不紊地查询学术数据库(PubMed 和 Semantic Scholar),收集关于基因组甲基化模式、基因表达变化及脱落酸水平的信息。每次查询后,系统返回相关文献,代理处理后生成后续查询,逐步深入。这展示了代理如何将复杂研究问题分解成逻辑步骤,先广泛搜索干旱条件下 DNA 甲基化,再缩小至具体基因,最后研究这些基因与 ABA 产生的关系。
章节10将讨论 LangSmith,但提前看一下其对我们图代理的瀑布图表示(见图5-4)也很有帮助。
LangGraph 代理的组织结构反映了不同的组织形式,其中多个节点(或部门)可以以多种方式相互作用,实现并行处理和迭代优化。图 5-5 从图结构的角度展示了几种最常见的组织结构类型:
- 层级结构(图 5-5a)可表示为一棵树状图,有一个根节点(通常是 CEO 或最高管理者),下属节点从根节点分支出来,代表不同管理层级和员工。层级结构的特点是指挥链条透明,汇报关系明确。
- 扁平结构(图 5-5b)可表示为层级较少或单层的图,大多数节点(员工)连接到一个中心节点(经理或协调者)。这种结构促进权力下放、跨职能协作和更快的决策。
- 矩阵结构(图 5-5c)表现为具有跨职能连接的图,员工根据其职能专长和项目任务可能向多个经理或项目负责人汇报。矩阵结构结合了职能型和产品/项目型结构的优势。
- 网络结构(图 5-5d)表现为一个去中心化的图,多个相互连接的节点(团队或个人)无缝协作和共享信息。网络结构常见于重视敏捷性、创新和知识共享的组织。
利用这些组织结构,我们可以创建多智能体协作,将复杂问题拆分为更小的子任务,并将每个子任务分配给专门的智能体或模型。关键思想是发挥不同 AI 智能体的优势,每个智能体都专注于某一特定任务或技能领域。通过协同组合多个专业智能体的能力,整体系统能够解决单个智能体难以应对的挑战。这是通过创建一个状态图(state graph),其中的节点代表各种参与的 AI 智能体和工具来实现的。
提示
类似于链(chains),在 Python 中调用 graph.get_graph(),并配合 draw_mermaid()、draw_png() 或 draw_mermaid_png() 等方法,可以可视化创建的图结构。
这些节点通过传递消息和状态数据相互通信,遵循预定义的逻辑,类似于示例 5-11。主要区别在于部分去中心化——每个节点可以作为一个智能体,在完成自己的部分后,将任务交接给下一个最合适的节点,而非每次迭代后都向智能体主管汇报。节点可根据需要调用工具和功能以协助完成子任务。有些节点负责收集数据,有些负责分析,有些生成可视化,等等。
这种协作式多智能体方法为复杂问题解决提供了一种高效的“分而治之”策略。通过协同组合不同 AI 组件的专业能力,整体系统的表现远超各个部分简单相加的效果。这一范式已成为开发复杂 AI 解决方案的有力方式,能够超越任何单一通用 AI 智能体或模型的表现。
在示例 5-11 中,决策智能体即为 agent 节点。一种有效策略是设置一个主管智能体,负责收集和分析其他智能体的结果,并基于权重做出下一步行动的决策——这类似于图 5-5b 所示的扁平结构。示例 5-13 和 5-14 构建了一个团队,负责搜索科学成果、进行可视化和生成总结报告。该团队的通信方案见图 5-6。
团队可以描述如下:
用户
用户发起请求,例如“列出10个在MMLU的STEM和人文学科部分有结果的LLM,生成一份包含不同模型分数的直方图和总体表格的报告,并在报告中包含发现的总结”。
主管(supervisor)
主管智能体根据下一步行动决定调用哪些节点(智能体)。如果需要从学术和研究数据库收集信息,则调用Researcher智能体;如果需要可视化,则调用VizGenerator智能体;如果需要生成报告,则调用ReportGenerator智能体。如果某些调用质量不佳,主管会再次调用对应的智能体。任务完成后,主管将发送FINISH指令给用户。
研究者(Researcher)
Researcher智能体接收主管的请求,触发提供的工具(如arxiv和semanticscholar)进行搜索,并将响应返回给主管。
可视化生成器(VizGenerator)
VizGenerator智能体接收主管请求,调用Python REPL生成并执行代码,将结果返回给主管。
报告生成器(ReportGenerator)
ReportGenerator智能体接收主管请求,调用write_report工具生成摘要,生成HTML文本,并保存到存储中,最后将响应返回给主管。
VizGenerator和ReportGenerator使用的工具示例代码:
from langchain_experimental.utilities import PythonREPL
# 初始化工具
repl = PythonREPL()
@tool
def python_repl(code: str):
"""执行Python代码,并打印输出。"""
try:
result = repl.run(code)
return f"""成功执行:
\n```python\n{code}```
\n标准输出:{result}"""
except BaseException as e:
return f"执行失败。错误:{repr(e)}"
class WriteReportArgsSchema(BaseModel):
filename: str
html: str
def write_report(filename, html):
with open(filename, 'w') as f:
f.write(html)
write_report_tool = StructuredTool.from_function(
name="write_report",
description="将HTML作为报告写入磁盘。",
func=write_report,
args_schema=WriteReportArgsSchema
)
示例5-13:团队及实例初始化
我们的团队成员包括Researcher、VizGenerator和ReportGenerator,由主管协调。
需要两个辅助函数:create_agent() 用于根据特定系统消息和工具集构建每个智能体;run_agent() 用于执行智能体并格式化响应。系统使用偏函数包装智能体执行,创建一个名为agent_nodes的字典,供工作流图使用。
主管通过基于提示的路由机制协调整个流程,决定下一个执行的智能体。主管链绑定函数定义到LLM,确保输出格式结构化,供JsonOutputFunctionsParser解析。这样设计使得智能体可以根据主管的路由决策依次工作,直到主管选择FINISH结束任务。
# 定义AgentState类
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
next: str
# 创建智能体的辅助函数
def create_agent(tools, system_message):
prompt = ChatPromptTemplate.from_messages([
("system", system_message),
MessagesPlaceholder(variable_name="messages"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = create_openai_tools_agent(llm, tools, prompt)
return AgentExecutor(agent=agent, tools=tools)
# 执行智能体的辅助函数
def run_agent(state, agent, name):
result = agent.invoke(state)
return {"messages": [HumanMessage(content=result["output"], name=name)]}
# 定义团队成员及创建智能体
members = ["Researcher", "VizGenerator", "ReportGenerator"]
research_agent = create_agent(
tools=load_tools(["arxiv"]) + [SemanticScholarQueryRun()],
system_message="提供准确数据以生成图表。"
)
chart_agent = create_agent(
tools=[python_repl],
system_message="生成用户可见的可视化和表格。"
)
report_agent = create_agent(
tools=[write_report_tool],
system_message="基于研究和可视化结果创建HTML报告。"
)
# 为每个智能体节点创建偏函数
agent_nodes = {
"Researcher": functools.partial(run_agent, agent=research_agent, name="Researcher"),
"VizGenerator": functools.partial(run_agent, agent=chart_agent, name="VizGenerator"),
"ReportGenerator": functools.partial(run_agent, agent=report_agent, name="ReportGenerator"),
}
# 主管路由函数定义
supervisor_prompt = ChatPromptTemplate.from_messages([
("system", """你管理以下工作人员间的对话:{members}。
根据用户请求,回应下一个应执行的工作人员。"""),
MessagesPlaceholder(variable_name="messages"),
("system", """下一个应执行谁?或者任务是否完成?请选择:{options}"""),
]).partial(options=str(["FINISH"] + members), members=", ".join(members))
function_def = {
"name": "route",
"description": "选择下一个角色。",
"parameters": {
"title": "routeSchema",
"type": "object",
"properties": {
"next": {
"title": "Next",
"anyOf": [{"enum": ["FINISH"] + members}],
}
},
"required": ["next"],
}
}
supervisor_chain = (
supervisor_prompt
| llm.bind_functions(functions=[function_def], function_call="route")
| JsonOutputFunctionsParser()
)
示例5-14:设计团队工作流
为工作流图添加主管节点和各专门智能体节点,设置边使所有智能体任务完成后均反馈给主管。
创建条件路由系统,主管根据当前状态决定下一个执行智能体,特殊状态FINISH则终止工作流。主管节点作为入口节点。
workflow = StateGraph(AgentState)
workflow.add_node("supervisor", supervisor_chain)
for member, node_func in agent_nodes.items():
workflow.add_node(member, node_func)
workflow.add_edge(member, "supervisor")
conditional_map = {member: member for member in members}
conditional_map["FINISH"] = END
workflow.add_conditional_edges("supervisor", lambda state: state["next"], conditional_map)
workflow.set_entry_point("supervisor")
graph = workflow.compile()
query = """列出10个在MMLU的STEM和人文学科部分有结果的LLM,
并生成包含不同模型分数直方图和总体表格的报告。
报告中包含发现总结。"""
for s in graph.stream(
{"messages": [HumanMessage(content=query)]},
{"recursion_limit": 100},
):
if "__end__" not in s:
print(s)
print("----")
图5-7展示了团队生成的报告示例。当然,如果你有预定义的报告模板,也可以设计或使用工具读取该模板并应用于最终文档。
组建 AI 团队时,可以参考类似于“贝尔宾九大团队角色”(Nine Belbin Team Roles)框架的角色分类,该框架描述了团队的动态运作。这些角色分为以行动为导向、以人为导向和以思考为导向三类,每类角色为团队带来独特的优势和贡献。在基于 LangGraph 的 AI 团队中,这些角色可以映射到 AI 驱动智能体的功能与能力上:
以行动为导向的角色
如塑造者(Shaper)、执行者(Implementer)和完成者-收尾者(Completer-Finisher)对推动进展和保证项目高效执行至关重要。在 AI 团队中,塑造者可以由 AI 项目经理或规划智能体来担当,动态调整策略和计划,这就是我们前面提到的主管角色。执行者则可以是专注于编码、系统集成或工具使用的 AI 智能体。完成者-收尾者负责输出的质量和准确性,通常由验证和测试智能体体现,负责严格检查模型输出中的错误和不一致。
以人为导向的角色
包括协调者(Coordinator)、团队工作者(Team Worker)和资源调查者(Resource Investigator),他们确保沟通顺畅和协作高效。虽然这些角色中带有“人”的元素,但在 AI 团队中同样适用。协调者可由管理工作流程、分配任务、保证团队目标一致的 AI 智能体来担任。目前我们是手动设置智能体、工具和通信机制,但未来可以由协调者智能体来自动完成。团队工作者推动合作,维护团队士气,这可以通过 AI 驱动的反馈系统来支持,未来可以进行相关尝试。资源调查者负责探索机会和外部资源,可以由扫描最新研究、工具和技术、为项目提供宝贵洞见的 AI 智能体承担,这种角色颇具未来感。
以思考为导向的角色
包括创新者(Plant)、监督评价者(Monitor Evaluator)和专家(Specialist),推动创新和战略思考。创新者是创造性思考者,可以由通过头脑风暴、模式识别等技术产生新思路和解决方案的 AI 智能体担任。第4章提到了提高模型“温度”能增强创造力的案例,这些智能体可能由不同领域微调模型和不同温度设置的模型驱动。监督评价者负责批判性分析想法和方案,这个角色可由负责数据分析和模型性能评估的 AI 智能体担任。考虑到 AI 智能体不同于人类,监督评价者角色可能与主管角色合并。专家提供某一领域的深度知识,可以由特定领域微调模型和 RAG 系统来实现。
备注
管理和划分团队角色有多种理论与实践方法,如 Margerison-Mccann、Strength Deployment Inventory 等。介绍贝尔宾团队角色旨在为 AI 团队设计提供潜在成员连接、功能及提示的思路。
AI 驱动的团队可应用于多种数据相关工作,如自动标注、数据提取、数据补全和合成。目前很多领域的应用仍然耗时且劳动强度大。作为一名博士生,我曾花近半年时间准备所有必要的文献综述。其他费时的任务还包括不同出版物格式的标准化以及幻灯片制作。后续章节将继续介绍构建不同架构和复杂度的专业研究团队,来解决各种任务。
模型上下文协议(Model Context Protocol,简称 MCP)
到目前为止,我们已经学习了如何构建链、智能体和多智能体团队。最大的挑战可能是获取特定的 API 和数据库访问权限,以及如何创建自己的工具。大多数情况下,你可能希望将现有代码或软件集成到团队中,那么有没有办法实现呢?
MCP 是由 Anthropic 创建的一个开放标准,旨在连接 AI 模型与外部数据和工具。其核心是一个客户端-服务器协议,允许 AI 助手访问训练数据之外的信息,并在现实世界中执行操作。MCP 不是让 AI 模型孤立工作,而是建立起连接各种数据源和工具的标准化桥梁,使 AI 更容易从文档、数据库或 API 中检索信息,并执行诸如发送邮件或更新文件等操作。MCP 通过定义一套明确的规则,规范 AI 如何发现、连接并使用外部工具。当你为特定工具或数据源(如文档库)设置 MCP 服务器时,AI 智能体可以通过标准化接口自动发现并使用这些功能。这大大简化了将 AI 模型连接到各种外部系统的过程,避免了以往开发有用 AI 应用时必须为每个集成编写定制代码的瓶颈。
MCP 服务器主要暴露三类资源:
- 工具(执行动作的函数)
- 资源(文档或文件)
- 提示(Prompts)
MCP 对 AI 助手最大的优势是能跨多个系统执行复杂的多步骤工作流程。第7章(专注于生物学)将展示一个例子:生成 DNA 序列和蛋白质结构,利用折叠算法计算结合性质,进行结果可视化,并计算不同蛋白质属性及突变影响。未来,MCP 有望通过单一接口让这类团队的开发更简单。
提示
为了更好理解 API 与 MCP 的区别,可以将传统 API 比作打开特定锁的专用钥匙,每把钥匙只适合一个锁,开发者为每个锁都要定制一把钥匙。而 MCP 更像是一把万能钥匙,可以打开多种锁。开发者无需为每个数据源和工具分别设计钥匙,只需使用 MCP 这把标准化的“万能钥匙”即可。
虽然 MCP 专注于连接 AI 智能体与外部工具和数据源,另一项新兴协议则关注 AI 智能体之间的直接协作。Agent2Agent(简称 A2A)是谷歌发起的开放协议,得到了50多家技术合作伙伴支持。A2A 设计目的是让不同技术或供应商的 AI 智能体可以互相通信、协调和协作。不同于 MCP 的工具聚焦方式,A2A 将智能体视为自治的对等体,允许他们发现彼此能力,交换结构化任务,并协同完成复杂工作流,无需共享内存、工具或执行计划。
A2A 协议满足企业环境中智能体互操作性的关键需求,在这些环境下,来自不同厂商的多个专用智能体必须协作。通过标准化的“智能体卡”(Agent Cards)描述能力,A2A 支持动态发现和任务委派。举例来说,在贷款审批工作流中,客户端智能体可以与风险评估、合规和支付智能体合作,这些智能体独立运行,但通过 A2A 结构化通信框架协调配合。该协议支持快速任务与可能持续数小时或数天的长流程操作,期间提供实时反馈和状态更新。这使得 A2A 在需要跨系统、跨部门协作的复杂企业工作流中尤为有价值。
MCP 与 A2A 的关键区别在于它们在智能体系统中的互补角色:
- MCP 擅长工具互操作性——连接智能体与结构化函数、API 及外部资源(如数据库、文档库)
- A2A 擅长智能体互操作性——使自治智能体作为对等体在自然语言及混合模式下协作
实际上,这两者可协同工作:智能体可能通过 MCP 访问信用评分 API 或交易数据,再通过 A2A 与其他智能体协作做决策和任务协调。MCP 构建智能体与外部系统间的桥梁,A2A 构建智能体间的桥梁,实现复杂多智能体协作,这对于企业级自动化尤为重要。
MCP 也有潜力改变研究人员与复杂数据及设备交互的方式。比如,整合来自不同实验平台、设备和分析工具的数据。借助 MCP,AI 助手可以无缝查询这些系统,整合结果并提供统一见解,显著加速研究进程。
此外,MCP 有望解决医疗领域长期存在的患者数据碎片化问题。医疗机构常常面临电子健康记录(EHR)系统之间互通性差、实验室系统独立存在、影像系统数据格式多样等问题。MCP 支持的 AI 助手可以同时连接多个医疗系统,为医生提供患者完整病史视图,无论数据存储在哪里。这理论上有助于提升诊断质量,减少重复检测,促进协同医疗。
MCP 还能助力远程患者监测和护理协调。慢性病患者常用多种家用监测设备(血压计、血糖仪等),同时看多个专家,且各专家系统独立。MCP 支持的系统可整合家用设备数据,连接不同提供者的电子记录系统,帮助跨团队协调护理。比如血压异常时,系统可自动提醒相应医生,安排随访,并确保团队成员共享相关信息——所有这些都通过标准化 MCP 连接实现,无需针对每个系统定制集成。
尽管前景广阔,MCP 仍面临挑战。管理多个 MCP 服务器可能带来运维负担,尤其是在对正常运行时间和安全性要求高的生产环境中。该协议最初设计用于本地和桌面场景,其在云架构和多用户环境中的扩展性仍有待验证。此外,工具使用的有效性也令人担忧——模型拥有更多工具不意味着它能有效利用。后续章节将展示 AI 模型在选择正确工具及正确调用工具方面仍有困难。
展望未来,MCP 正快速发展以克服上述限制。随着协议成熟和广泛采用,它有望变革 AI 系统与现实世界的交互方式,使其更智能、更具上下文感知,并广泛适用于多种场景。MCP 由社区驱动,其发展以真实需求和用例为导向,确保持续演进以创造实际价值。
总结
本章探讨了如何使用 LangChain 构建个人 AI 助手,重点介绍了链(chains)、智能体(agents)和多智能体团队。章节解释了 LangChain 中链作为灵活的流水线,能够将复杂任务拆解成更小的步骤。通过诸如基于 RAG 增强的链和辩论原型等示例,您学习了如何为不同的研究任务设计链结构。
随后,章节超越了简单链,介绍了智能体——能够调用工具并自主决定操作顺序的 AI 组件。其中一个例子是科学检索智能体,能够集成 Scopus、arXiv、PubMed 和 Semantic Scholar 等学术数据库,展示了 AI 如何通过自动收集和分析科学文献辅助科研。
在单一智能体基础上,章节引入了 LangGraph,构建专门的多智能体团队协同工作。这些多智能体系统能够通过合作解决复杂的科研问题。章节提供了一个实用案例,展示了不同智能体分别负责文献检索、数据可视化和报告生成,且由一个监督智能体协调它们的工作。
最后,章节介绍了模型上下文协议(Model Context Protocol,MCP),这是一个用于连接 AI 模型与外部数据源和工具的开放标准。MCP 为 AI 助手提供了访问训练数据之外信息和执行现实操作的标准化方式,有望变革研究人员与复杂数据及设备的交互方式。
至此,第一部分内容结束。第二部分章节将涵盖多个生命科学领域下的 LangChain 流水线和 LangGraph 团队的实例演示。