
AI Agents-5 | AI工作流与代理的深度剖析:从原理到实战应用 原创
这个系列文章旨在为AI代理(AI Agent)提供全面的概述,深入研究其特征,组成部分和类型,同时探索其进化,挑战和潜在的未来方向。
在人工智能飞速发展的今天,我们常常听到“AI工作流”和“AI代理”这两个概念。但你知道它们之间有什么区别吗?又该如何选择适合自己的技术路径呢?今天,我们就来深入探讨一下。
一、工作流与代理:到底有什么区别?
(一)定义与区别
在AI的世界里,“代理”(Agent)和“工作流”(Workflow)是两种截然不同的存在。有些人把代理定义为完全自主的系统,它们可以独立运行很长时间,利用各种工具完成复杂的任务。而另一些人则用它来描述那些按照预定义流程执行的系统。
Anthropic公司把这两种情况都归类为“代理系统”,但他们明确区分了工作流和代理:
- 工作流:是一种通过预定义代码路径来协调LLM(大型语言模型)和工具的系统。
- 代理:则是LLM动态地指导自己的流程和工具使用,完全掌控任务的执行方式。
用一个简单的比喻来说,工作流就像是按照食谱一步步做饭,而代理则像是一个厨师,根据食材和口味现场决定怎么做菜。
(二)什么时候用代理,什么时候不用?
开发基于LLM的应用时,最好从最简单的解决方案开始,只有在必要时才引入复杂性。有时候,完全避开代理系统可能是个更好的选择。毕竟,这些系统虽然能提高任务性能,但往往伴随着更高的延迟和成本。所以,我们需要权衡利弊。
如果任务结构清晰、规则明确,工作流可以提供稳定性和一致性。而代理则更适合那些需要灵活性和大规模模型驱动决策的场景。不过,对于很多应用来说,优化单个LLM调用,加上检索和上下文示例,往往就足够了。
(三)框架的使用:利与弊
现在有很多框架可以让代理系统的实现变得更简单,比如LangChain的LangGraph、亚马逊Bedrock的AI代理框架、Rivet(一个拖拽式GUI的LLM工作流构建器)和Vellum(用于构建和测试复杂工作流的GUI工具)。这些框架简化了调用LLM、定义和解析工具、串联调用等标准低级任务,让你更容易上手。
但这些框架也有缺点。它们可能会增加额外的抽象层,让你看不清底层的提示和响应,从而让调试变得困难。而且,它们可能会让你不自觉地增加复杂性,而实际上简单的设置就足够了。
所以,我们建议开发者先直接使用LLM的API,因为很多模式只需要几行代码就能实现。如果你决定使用框架,一定要彻底理解底层代码,因为对框架内部工作原理的错误假设是错误的常见来源。
二、构建模块:从简单到复杂
(一)基础设置
在构建代理系统时,你可以使用任何支持结构化输出和工具调用的聊天模型。以下是一个简单的设置过程,展示了如何安装包、设置API密钥,并测试Anthropic的结构化输出和工具调用。
import os
import getpass
from langchain_anthropic import ChatAnthropic
def _set_env(var: str):
if not os.environ.get(var):
os.environ[var] = getpass.getpass(f"{var}: ")
_set_env("ANTHROPIC_API_KEY")
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
(二)增强型LLM:核心构建模块
代理系统的基础是一个增强型LLM,它通过检索、工具和记忆等增强功能来实现更强大的能力。我们的模型可以主动利用这些能力——生成自己的搜索查询、选择合适的工具、决定保留哪些信息。
我们建议重点关注两个方面:一是将这些能力定制化到你的具体用例中;二是确保它们为LLM提供一个易于使用且文档齐全的接口。虽然实现这些增强功能有很多方法,但一种方法是通过我们最近发布的模型上下文协议(Model Context Protocol),它允许开发者通过简单的客户端实现与不断增长的第三方工具生态系统集成。
从现在开始,我们假设每个LLM调用都能访问这些增强功能。
(三)工作流的实现
1. 链式提示(Prompt Chaining)
链式提示是一种将任务分解为一系列步骤的工作流,每个LLM调用都处理前一个的输出。你可以在这个过程中加入程序化检查(见下图中的“门”),以确保流程仍在正轨上。
这种工作流适用于那些可以轻松分解为固定子任务的场景。主要目标是通过增加延迟来换取更高的准确性,让每个LLM调用的任务变得更简单。
例如:
- 先生成营销文案,然后将其翻译成另一种语言。
- 先写文档大纲,检查大纲是否符合某些标准,再根据大纲撰写文档。
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from IPython.display import Image, display
# 图状态
class State(TypedDict):
topic: str
joke: str
improved_joke: str
final_joke: str
# 节点
def generate_joke(state: State):
"""第一次LLM调用,生成初始笑话"""
msg = llm.invoke(f"Write a short joke about {state['topic']}")
return {"joke": msg.content}
def check_punchline(state: State):
"""检查笑话是否有笑点"""
if"?"in state["joke"] or"!"in state["joke"]:
return"Fail"
return"Pass"
def improve_joke(state: State):
"""第二次LLM调用,改进笑话"""
msg = llm.invoke(f"Make this joke funnier by adding wordplay: {state['joke']}")
return {"improved_joke": msg.content}
def polish_joke(state: State):
"""第三次LLM调用,完善笑话"""
msg = llm.invoke(f"Add a surprising twist to this joke: {state['improved_joke']}")
return {"final_joke": msg.content}
# 构建工作流
workflow = StateGraph(State)
# 添加节点
workflow.add_node("generate_joke", generate_joke)
workflow.add_node("improve_joke", improve_joke)
workflow.add_node("polish_joke", polish_joke)
# 添加边连接节点
workflow.add_edge(START, "generate_joke")
workflow.add_conditional_edges(
"generate_joke", check_punchline, {"Fail": "improve_joke", "Pass": END}
)
workflow.add_edge("improve_joke", "polish_joke")
workflow.add_edge("polish_joke", END)
# 编译
chain = workflow.compile()
# 显示工作流
display(Image(chain.get_graph().draw_mermaid_png()))
# 调用
state = chain.invoke({"topic": "cats"})
print("初始笑话:")
print(state["joke"])
print("\n--- --- ---\n")
if"improved_joke"in state:
print("改进后的笑话:")
print(state["improved_joke"])
print("\n--- --- ---\n")
print("最终笑话:")
print(state["final_joke"])
else:
print("笑话未通过质量检查——未检测到笑点!")
2. 并行化(Parallelization)
并行化是一种让LLM同时处理任务的工作流,其输出可以通过程序化的方式聚合。这种工作流有两种主要形式:分段(Sectioning)和投票(Voting)。
- 分段:将任务分解为独立的子任务并并行运行。
- 投票:多次运行相同的任务以获得多样化的输出。
这种工作流适用于以下场景:当子任务可以并行化以提高速度时,或者需要多种视角或尝试以获得更可靠的结果时。对于复杂的任务,如果每个考虑因素都由单独的LLM调用处理,LLM通常会表现得更好,因为这样可以让每个调用专注于特定的方面。
例如:
- 分段:实现防护栏,一个模型实例处理用户查询,另一个筛选不当内容或请求。这比让同一个LLM调用处理防护栏和核心响应表现得更好。
- 投票:审查代码漏洞,多个不同的提示检查并标记问题;评估内容是否不当,多个提示从不同角度评估,或需要不同的投票阈值以平衡误报和漏报。
# 图状态
class State(TypedDict):
topic: str
joke: str
story: str
poem: str
combined_output: str
# 节点
def call_llm_1(state: State):
"""第一次LLM调用,生成笑话"""
msg = llm.invoke(f"Write a joke about {state['topic']}")
return {"joke": msg.content}
def call_llm_2(state: State):
"""第二次LLM调用,生成故事"""
msg = llm.invoke(f"Write a story about {state['topic']}")
return {"story": msg.content}
def call_llm_3(state: State):
"""第三次LLM调用,生成诗歌"""
msg = llm.invoke(f"Write a poem about {state['topic']}")
return {"poem": msg.content}
def aggregator(state: State):
"""将笑话、故事和诗歌合并为一个输出"""
combined = f"Here's a story, joke, and poem about {state['topic']}!\n\n"
combined += f"STORY:\n{state['story']}\n\n"
combined += f"JOKE:\n{state['joke']}\n\n"
combined += f"POEM:\n{state['poem']}"
return {"combined_output": combined}
# 构建工作流
parallel_builder = StateGraph(State)
# 添加节点
parallel_builder.add_node("call_llm_1", call_llm_1)
parallel_builder.add_node("call_llm_2", call_llm_2)
parallel_builder.add_node("call_llm_3", call_llm_3)
parallel_builder.add_node("aggregator", aggregator)
# 添加边连接节点
parallel_builder.add_edge(START, "call_llm_1")
parallel_builder.add_edge(START, "call_llm_2")
parallel_builder.add_edge(START, "call_llm_3")
parallel_builder.add_edge("call_llm_1", "aggregator")
parallel_builder.add_edge("call_llm_2", "aggregator")
parallel_builder.add_edge("call_llm_3", "aggregator")
parallel_builder.add_edge("aggregator", END)
# 编译工作流
parallel_workflow = parallel_builder.compile()
# 显示工作流
display(Image(parallel_workflow.get_graph().draw_mermaid_png()))
# 调用
state = parallel_workflow.invoke({"topic": "cats"})
print(state["combined_output"])
3. 路由(Routing)
路由工作流可以根据输入的分类将其导向后续任务。这种工作流允许分离关注点,并构建更专业的提示。如果没有这种工作流,优化一种输入的性能可能会损害其他输入的性能。
这种工作流适用于以下场景:当任务复杂且有明确的类别,这些类别可以分别处理,并且分类可以准确完成(无论是通过LLM还是更传统的分类模型/算法)。
例如:
- 将不同类型的客户服务查询(一般问题、退款请求、技术支持)导向不同的下游流程、提示和工具。
- 将简单/常见问题导向较小的模型(如Claude 3.5 Haiku),将复杂/罕见问题导向更强大的模型(如Claude 3.5 Sonnet),以优化成本和速度。
from typing_extensions import Literal
from langchain_core.messages import HumanMessage, SystemMessage
# 结构化输出的模式,用于路由逻辑
class Route(BaseModel):
step: Literal["poem", "story", "joke"] = Field(
None, descriptinotallow="路由过程中的下一步"
)
# 为LLM添加结构化输出模式
router = llm.with_structured_output(Route)
# 状态
class State(TypedDict):
input: str
decision: str
output: str
# 节点
def llm_call_1(state: State):
"""写一个故事"""
result = llm.invoke(state["input"])
return {"output": result.content}
def llm_call_2(state: State):
"""写一个笑话"""
result = llm.invoke(state["input"])
return {"output": result.content}
def llm_call_3(state: State):
"""写一首诗"""
result = llm.invoke(state["input"])
return {"output": result.content}
def llm_call_router(state: State):
"""根据输入将任务路由到相应的节点"""
decision = router.invoke(
[
SystemMessage(
cnotallow="根据用户请求将输入路由到故事、笑话或诗歌。"
),
HumanMessage(cnotallow=state["input"]),
]
)
return {"decision": decision.step}
# 条件边函数,根据路由决策将任务导向相应节点
def route_decision(state: State):
if state["decision"] == "story":
return"llm_call_1"
elif state["decision"] == "joke":
return"llm_call_2"
elif state["decision"] == "poem":
return"llm_call_3"
# 构建工作流
router_builder = StateGraph(State)
# 添加节点
router_builder.add_node("llm_call_1", llm_call_1)
router_builder.add_node("llm_call_2", llm_call_2)
router_builder.add_node("llm_call_3", llm_call_3)
router_builder.add_node("llm_call_router", llm_call_router)
# 添加边连接节点
router_builder.add_edge(START, "llm_call_router")
router_builder.add_conditional_edges(
"llm_call_router",
route_decision,
{
"llm_call_1": "llm_call_1",
"llm_call_2": "llm_call_2",
"llm_call_3": "llm_call_3",
},
)
router_builder.add_edge("llm_call_1", END)
router_builder.add_edge("llm_call_2", END)
router_builder.add_edge("llm_call_3", END)
# 编译工作流
router_workflow = router_builder.compile()
# 显示工作流
display(Image(router_workflow.get_graph().draw_mermaid_png()))
# 调用
state = router_workflow.invoke({"input": "Write me a joke about cats"})
print(state["output"])
4. 协调者-工作者(Orchestrator-Worker)
在协调者-工作者工作流中,一个中心LLM动态地分解任务,将其分配给工作者LLM,并整合它们的结果。
这种工作流适用于以下场景:当任务复杂且无法预测需要哪些子任务时(例如在编程中,需要更改的文件数量以及每个文件的更改性质通常取决于具体任务)。虽然它在拓扑结构上与并行化类似,但关键区别在于其灵活性——子任务不是预先定义的,而是由协调者根据具体输入动态确定的。
举个例子,这种工作流非常适合以下场景:
- 编程产品需要对多个文件进行复杂更改,每次更改都可能涉及不同的任务。
- 搜索任务需要从多个来源收集和分析信息,以获取可能相关的数据。
from typing import Annotated, List
import operator
# 用于规划的结构化输出模式
class Section(BaseModel):
name: str = Field(descriptinotallow="报告部分的名称")
description: str = Field(descriptinotallow="本节涵盖的主要主题和概念的简要概述")
class Sections(BaseModel):
sections: List[Section] = Field(descriptinotallow="报告的各个部分")
# 为LLM添加结构化输出模式
planner = llm.with_structured_output(Sections)
# 创建工作者节点
因为协调者-工作者工作流非常常见,LangGraph提供了Send API来支持这种模式。它允许你动态创建工作者节点,并为每个节点分配特定的输入。每个工作者都有自己的状态,并且所有工作者的输出都会写入一个共享的状态键,协调者图可以访问这个键。这使得协调者能够访问所有工作者的输出,并将它们整合成最终输出。如下所示,我们遍历一个部分列表,并将每个部分发送到一个工作者节点。
```python
from langgraph.constants import Send
# 图状态
class State(TypedDict):
topic: str # 报告主题
sections: list[Section] # 报告部分列表
completed_sections: Annotated[list, operator.add] # 所有工作者并行写入此键
final_report: str # 最终报告
# 工作者状态
class WorkerState(TypedDict):
section: Section
completed_sections: Annotated[list, operator.add]
# 节点
def orchestrator(state: State):
"""协调者生成报告计划"""
report_sections = planner.invoke(
[
SystemMessage(cnotallow="生成报告计划"),
HumanMessage(cnotallow=f"报告主题:{state['topic']}")
]
)
return {"sections": report_sections.sections}
def llm_call(state: WorkerState):
"""工作者撰写报告的一部分"""
section = llm.invoke(
[
SystemMessage(cnotallow="根据提供的名称和描述撰写报告部分。每个部分不加前言,使用Markdown格式。"),
HumanMessage(cnotallow=f"部分名称:{state['section'].name},描述:{state['section'].description}")
]
)
return {"completed_sections": [section.content]}
def synthesizer(state: State):
"""将各部分整合成完整报告"""
completed_report_sections = "\n\n---\n\n".join(state["completed_sections"])
return {"final_report": completed_report_sections}
# 条件边函数,为每个计划部分分配工作者
def assign_workers(state: State):
"""为计划中的每个部分分配工作者"""
return [Send("llm_call", {"section": s}) for s in state["sections"]]
# 构建工作流
orchestrator_worker_builder = StateGraph(State)
# 添加节点
orchestrator_worker_builder.add_node("orchestrator", orchestrator)
orchestrator_worker_builder.add_node("llm_call", llm_call)
orchestrator_worker_builder.add_node("synthesizer", synthesizer)
# 添加边连接节点
orchestrator_worker_builder.add_edge(START, "orchestrator")
orchestrator_worker_builder.add_conditional_edges(
"orchestrator", assign_workers, ["llm_call"]
)
orchestrator_worker_builder.add_edge("llm_call", "synthesizer")
orchestrator_worker_builder.add_edge("synthesizer", END)
# 编译工作流
orchestrator_worker = orchestrator_worker_builder.compile()
# 显示工作流
display(Image(orchestrator_worker.get_graph().draw_mermaid_png()))
# 调用
state = orchestrator_worker.invoke({"topic": "创建关于LLM扩展规律的报告"})
# 显示最终报告
from IPython.display import Markdown
Markdown(state["final_report"])
5. 评估器-优化器(Evaluator-Optimizer)
在评估器-优化器工作流中,一个LLM调用生成响应,另一个提供评估和反馈,并在循环中不断优化。
这种工作流特别适用于以下场景:当我们有明确的评估标准,并且迭代优化能够带来显著价值时。如果LLM的响应可以在人类明确反馈后得到改进,并且LLM能够提供这种反馈,那么这种工作流就非常适合。这类似于人类作家在撰写一篇经过精心打磨的文档时所经历的迭代写作过程。
例如:
- 文学翻译中,翻译LLM可能无法在一开始就捕捉到所有细微差别,但评估LLM可以提供有用的批评。
- 复杂的搜索任务需要多轮搜索和分析以收集全面信息,评估器决定是否需要进一步搜索。
# 图状态
class State(TypedDict):
joke: str
topic: str
feedback: str
funny_or_not: str
# 用于评估的结构化输出模式
class Feedback(BaseModel):
grade: Literal["funny", "not funny"] = Field(descriptinotallow="判断笑话是否有趣")
feedback: str = Field(descriptinotallow="如果笑话不好笑,提供改进建议")
# 为LLM添加结构化输出模式
evaluator = llm.with_structured_output(Feedback)
# 节点
def llm_call_generator(state: State):
"""LLM生成笑话"""
if state.get("feedback"):
msg = llm.invoke(
f"根据反馈生成关于{state['topic']}的笑话:{state['feedback']}"
)
else:
msg = llm.invoke(f"生成关于{state['topic']}的笑话")
return {"joke": msg.content}
def llm_call_evaluator(state: State):
"""LLM评估笑话"""
grade = evaluator.invoke(f"评估笑话:{state['joke']}")
return {"funny_or_not": grade.grade, "feedback": grade.feedback}
# 条件边函数,根据评估器的反馈决定是否返回生成器或结束
def route_joke(state: State):
"""根据评估器的反馈决定是否返回生成器或结束"""
if state["funny_or_not"] == "funny":
return"Accepted"
elif state["funny_or_not"] == "not funny":
return"Rejected + Feedback"
# 构建工作流
optimizer_builder = StateGraph(State)
# 添加节点
optimizer_builder.add_node("llm_call_generator", llm_call_generator)
optimizer_builder.add_node("llm_call_evaluator", llm_call_evaluator)
# 添加边连接节点
optimizer_builder.add_edge(START, "llm_call_generator")
optimizer_builder.add_edge("llm_call_generator", "llm_call_evaluator")
optimizer_builder.add_conditional_edges(
"llm_call_evaluator",
route_joke,
{
"Accepted": END,
"Rejected + Feedback": "llm_call_generator"
}
)
# 编译工作流
optimizer_workflow = optimizer_builder.compile()
# 显示工作流
display(Image(optimizer_workflow.get_graph().draw_mermaid_png()))
# 调用
state = optimizer_workflow.invoke({"topic": "Cats"})
print(state["joke"])
三、代理(Agent):自主智能体的力量
代理通常被实现为LLM通过工具调用(基于环境反馈)在循环中执行动作的系统。正如Anthropic博客所指出的,代理可以处理复杂的任务,但其实现往往非常简单。它们通常是LLM根据环境反馈使用工具的循环。因此,清晰且周到地设计工具集及其文档至关重要。
什么时候使用代理?
代理适用于那些开放性问题,这些问题很难或无法预测所需的步骤数量,并且无法硬编码固定路径。LLM可能会运行多个回合,因此你需要对其决策能力有一定的信任。代理的自主性使其非常适合在受信任的环境中扩展任务。
然而,自主性也意味着更高的成本和可能出现的错误累积。因此,我们建议在沙盒环境中进行广泛的测试,并设置适当的防护栏。
例如:
- 编程代理可以解决涉及对多个文件进行编辑的任务。
- “计算机使用”参考实现中,Claude通过计算机完成任务。
from langchain_core.tools import tool
# 定义工具
@tool
def multiply(a: int, b: int) -> int:
"""乘法工具"""
return a * b
@tool
def add(a: int, b: int) -> int:
"""加法工具"""
return a + b
@tool
def divide(a: int, b: int) -> float:
"""除法工具"""
return a / b
# 为LLM绑定工具
tools = [add, multiply, divide]
tools_by_name = {tool.name: tool for tool in tools}
llm_with_tools = llm.bind_tools(tools)
四、结合与定制这些模式:实现最佳效果
AI代理和代理工作流是互补的,可以集成在一起以实现最佳效果,尤其是在复杂的现实世界应用中。
(一)增强自动化
AI代理可以自主处理特定任务,而代理工作流则将这些任务协调成一个连贯、高效的过程。
(二)可扩展性
在结构化工作流中结合多个AI代理,可以使组织高效扩展运营,减少人工工作量,提高生产力。
(三)弹性与适应性
虽然单个代理可以应对局部变化,但工作流可以动态调整整体流程,以与战略目标保持一致或适应外部干扰。
(四)实际案例:制造业中的集成AI代理与工作流
在智能制造系统中:
- AI代理可以监控设备性能、预测维护需求并优化生产计划。
- 代理工作流则负责原材料采购、生产排序、质量保证和物流,确保从原材料到产品交付的无缝过渡。
五、总结:选择适合你的系统才是成功的关键
在LLM领域,成功并不是关于构建最复杂的系统,而是构建最适合你需求的系统。从简单的提示开始,通过全面评估进行优化,只有在简单解决方案不足时才添加多步代理系统。
在实现代理时,我们建议遵循以下三个核心原则:
- 保持代理设计的简洁性:避免不必要的复杂性,专注于核心功能。
- 优先考虑透明性:明确展示代理的规划步骤,让用户清楚了解其决策过程。
- 精心设计代理-计算机接口(ACI):通过彻底的工具文档和测试,确保代理与外部系统的无缝交互。
框架可以帮助你快速上手,但不要害怕在进入生产阶段时减少抽象层,直接使用基础组件。遵循这些原则,你可以创建出不仅强大而且可靠、可维护且值得用户信赖的代理系统。
在这个充满可能性的AI时代,选择合适的技术路径并将其应用于实际场景,才是实现智能化转型的关键。希望这篇文章能帮助你在AI工作流与代理的世界中找到属于你的方向,解锁智能技术的无限可能。
本文转载自公众号Halo咯咯 作者:基咯咯
