在本书中,我们已经探讨了支撑智能体 AI(agentic AI)系统的概念、架构与模式。我们审视了它们的构建模块、它们如何交互,以及如何通过协调来应对复杂任务。然而,要把这些理论构件转化为可运行、可生产落地的应用,还需要实用的工具与库来处理智能体创建、执行与通信背后的底层复杂性。这正是智能体框架发挥作用的地方。
从零开始构建一个智能体系统,意味着要管理大量“运动部件”:定义智能体的角色与能力、处理状态与记忆、编排工作流、集成 LLM、管理工具使用(如函数调用),以及促成智能体之间的通信。
智能体框架提供抽象与预构建组件来简化这些工作,使开发者可以把精力放在应用逻辑与智能体能力上,而不是重复造基础“管道”轮子。它们为智能体开发中的常见挑战提供结构化的方法,促进更快的开发节奏与更易维护的代码,并且往往会内置交互与控制方面的最佳实践。
在快速演进的智能体 AI 领域,已经出现了多个框架,每个都有自己的理念、优势与目标用例。尽管我们会聚焦三个有代表性的例子来展示实现方式,但需要强调的是:这些例子是用于说明,并非穷举;本书讨论的底层设计模式是通用的,也可以迁移到其他框架之上。
在本章中,我们将对三个典型示例做一个实践导入:
- Google 的 Agent Development Kit(ADK)
- CrewAI
- LangGraph
我们会先简要介绍每个框架,突出其核心概念。随后,我们将比较它们的相同点与差异,帮助你理解各自的设计哲学。为了让比较更具体,我们会使用 CrewAI 与 LangGraph 重新实现第 13 章的“贷款处理”智能体用例。由于前几章我们已经大量使用 Google 的 ADK 来建立基线架构,因此这里不再重复该实现;相反,我们会基于随书配套 Notebook 中的代码,重点展示同一个问题如何在不同框架范式下被实现。最后,我们会讨论在项目中选择智能体框架时的关键考量:强调框架能力与具体用例对齐的重要性,以及无论选择什么工具,都需要持续具备强健的可观测性并遵循负责任 AI 原则。
既然我们已经理解了框架在构建智能体系统中的重要性,那么就从第一个示例开始更深入地看:Google 的 ADK。
技术要求
为了更好地跟上本章的实践示例,你需要准备以下内容:
- 一个 Python 环境(版本 3.10+)
- 一个 Jupyter Notebook 界面(例如 JupyterLab、Google Colab 或 VS Code)
- 一个已启用 Vertex AI API 的 Google Cloud 项目,或一个 Google AI Studio API Key
- 安装以下 Python 库:crewai、langgraph、langchain-google-genai、google-cloud-aiplatform
本章完整代码位于本书 GitHub 仓库的以下目录中:https://github.com/PacktPublishing/Agentic-Architectural-Patterns-for-Building-Multi-Agent-Systems/tree/main/Chapter_15。
既然我们已经理解了框架在构建智能体系统中的重要性,那么就从第一个示例开始更深入地看:Google 的 ADK。
Google 的 Agent Development Kit(ADK)
正如我们在前面的示例中看到的,Google 的 ADK 为构建与部署 AI 智能体提供了一个结构化环境。它以生产就绪为目标进行设计,提供了用于定义智能体、管理其生命周期、集成工具以及促进通信(尤其是在多智能体场景中)的组件。
ADK 的目标是为构建能够可靠推理、规划,并与外部系统及其他智能体交互的智能体提供基础“脚手架”。其关键特性通常包括:
- 智能体抽象(Agent abstraction) :用于定义智能体核心逻辑的基类或结构,包括其指令、工具,以及如何处理传入任务或消息。
- 工具集成(Tool integration) :用于定义与注册工具(通常是函数或 API)的机制,使智能体能够与外部世界交互或执行特定动作,这与“智能体使用工具在环境中行动”的概念一致。
- 规划与推理(Planning and reasoning) :与 LLM(如 Gemini)集成,为智能体的推理循环提供动力:处理信息、规划步骤,并决定何时使用工具。ADK 往往提供内置规划器,或允许自定义规划逻辑。
- 状态管理(State management) :让智能体在多轮交互中保持状态与记忆的机制,这对于复杂的多步骤任务至关重要。
- 通信协议(Communication protocol) :原生支持 Agent-to-Agent(A2A)互操作协议。ADK 使智能体能够使用 A2A 的标准化消息格式与任务生命周期,在不同框架与企业边界之间进行通信与协作。
- 运行时环境(Runtime environment) :一个执行环境(agent runtime 或 agent engine),负责管理智能体部署、任务分发、并行执行与重试,并可能与可观测性工具集成。
第 13 章与第 14 章的贷款处理示例展示了:ADK 如何将不同的智能体能力(文档校验、征信检查、风险评估、合规检查)定义为工具,并通过一个由 LLM 驱动、遵循特定指令的智能体来编排它们的执行。ADK 提供了结构化能力,使你可以在受管运行时中构建这种面向目标、会使用工具的智能体。
CrewAI
CrewAI 从另一个视角构建智能体系统:它显式强调通过“角色扮演智能体”实现协作智能,把多个自主 AI 智能体编排成一个协同工作的“团队(crew)”。
CrewAI 的核心理念是:复杂任务往往可以被拆分并分配给具有特定角色、职责,甚至“背景故事(backstories)”的智能体,这些设定会引导其行为与专长。随后,这些智能体通过协作共享信息与中间结果,以达成共同目标。
CrewAI 的关键概念包括:
- Agents(智能体) :以特定角色、目标与背景故事来定义,同时指定其使用的 LLM,并可配置其可访问的工具。角色扮演的方式有助于让 LLM 体现某种人格或专业能力。
- Tasks(任务) :分配给智能体的具体工作。每个任务包含描述与期望输出,并绑定给某个特定智能体。任务可以串联,使一个任务的输出成为下一个任务的输入。
- Tools(工具) :与其他框架类似,是智能体用来与外部系统交互或执行动作的函数或能力(例如网页搜索、调用 API)。在 CrewAI 中,工具通常继承自 BaseTool 类。
- Crew(团队) :智能体集合以及它们需要执行的任务集合。Crew 定义了智能体如何协作。
- Process(流程) :团队执行任务时遵循的工作流或方法论。常见流程包括顺序执行(sequential:任务按顺序逐个执行)或分层执行(hierarchical:由“经理”智能体分派任务)。
CrewAI 强调智能体交互的“社会性”,使得设计“不同 AI 人设提供专长技能”的系统更直观,类似人类团队协作方式。它通过提供高层抽象(角色定义与协作工作流管理)来简化多智能体系统的构建。
不过,对于企业架构师而言,一个重要考量是:这种“人设驱动(persona-driven)”风格可能会引入输出的波动性。当将 CrewAI 应用于受监管或高风险流程(例如我们的贷款裁决用例)时,就必须用严格的工具契约与严密测试来对冲这种灵活性,以确保结果具有确定性并满足合规要求。
LangGraph
LangGraph 是对 LangChain 库的扩展,提供了一种基于“图(graph)”来构建有状态(stateful)、多参与者应用(包括复杂智能体系统)的稳健方式。LangChain 更偏向“链式调用”(线性序列),而 LangGraph 支持环路(cycles),因此更适合对智能体行为进行更灵活的建模——因为智能体往往需要循环、重试,或根据当前状态动态决定下一步。
LangGraph 将智能体工作流表示为状态机:工作流中的每一步是图中的一个节点(node),步骤之间的转移是边(edge)。这种图结构会显式管理应用状态随执行过程的演化。
LangGraph 的关键概念包括:
- StateGraph:代表工作流图的核心对象,承载应用的状态。
- State(状态) :一个定义好的数据结构(通常是 Python 类或字典,例如 TypedDict),包含与工作流进展相关的所有信息(如用户输入、中间结果、智能体消息)。
- Nodes(节点) :表示图中步骤或参与者(智能体)的函数或可运行对象。每个节点接收当前状态,执行一个动作(例如调用 LLM、使用工具或处理数据),并返回对状态的更新。
- Edges(边) :定义节点间的转移关系。边依据当前状态或前一节点的输出决定下一步执行哪个节点。
- Conditional edges(条件边) :用于分支逻辑。基于当前状态或节点输出,图可以将执行路由到不同的后续节点,从而支持复杂决策与循环。
LangGraph 特别适用于以下场景:
- 必须显式管理状态
- 需要循环流程(例如智能体对输出进行反思并重试,或引入 Human-in-the-Loop(HITL)交互)
- 需要包含分支与动态路由的复杂控制流
- 需要对多个智能体或参与者(包括人类)之间的交互进行建模
通过把智能体交互表示为图,LangGraph 对执行流与状态持久化提供了细粒度控制,使其成为构建复杂且可靠的智能体应用的强大工具。
现在我们已经看到这些框架各自能够做什么了,接下来我们将重点梳理它们之间的相同点与差异。
三种框架的相同点与差异(Similarities and differences between the three frameworks)
尽管三种框架都旨在帮助你构建复杂的智能体应用,但它们底层的理念与架构使它们在不同方向上各有优势。
相同点(Similarities)
三种框架共享同一套概念基础:
- LLM 作为推理引擎:在核心层面,这三种框架都使用 LLM(例如 Gemini、GPT-4,或开源模型)作为智能体的“脑”。LLM 负责基于提示词、当前状态与可用工具进行推理、规划,并决定下一步动作。
- 工具集成:它们都围绕“函数调用(Function Calling)/工具使用(Tool Use)”模式构建——我们已将其视为智能体 AI 的基础。智能体“作用于世界”的能力(例如查询数据库、读取文件或调用 API)来自于为其提供一组工具。三种框架都提供结构化方式来定义这些工具,并将它们暴露给 LLM。
- 目标导向:它们不是用于简单的一次性问答(single-shot Q&A),而是为构建能够执行复杂、多步骤任务、以达成开发者定义目标的应用而设计。
关键差异(Key differences)
主要差异在于它们的核心抽象——也就是用于表示智能体工作流的“心智模型”。这一根本差异会影响从控制流到状态管理的全部设计。为了帮助你评估哪种心智模型最适合你的用例,我们来看每个框架如何理解其主要构建模块与运行逻辑:
核心理念与抽象(Core philosophy and abstraction)
-
CrewAI —— 基于角色的协作:CrewAI 的抽象是“专家团队”。你通过角色(如“资深贷款专员”)、目标(如“分析一份贷款申请”)和背景故事(如“你是一名一丝不苟的分析师……”)来定义智能体。这非常适合模拟人类团队的工作流,协作是其中心特性。
-
LangGraph —— 有状态图:LangGraph 的抽象是流程图或状态机。你定义节点(智能体或函数)与边(节点之间路径)。这会把关注点从“智能体”转向“流程”。它的优势在于让应用状态显式化,并使控制流更具确定性(deterministic)。
-
Google ADK —— 面向生产的智能体:ADK 的抽象是“智能体本体”,将其视为可模块化、可测试、可部署的软件组件。它更偏代码优先(code-first),对软件工程师更熟悉。它聚焦智能体生命周期,并依赖两个关键机制获得企业级鲁棒性:
- Callbacks(用于主动过滤、PII 检测与 HITL 控制的中间件能力)
- Workflow agents(用于定义顺序/循环/并行任务的脚手架,同时结合自主推理)
控制流与循环行为(Control flow and cyclical behavior)
- CrewAI 在高层管理控制流:通常定义为顺序(task1 → task2 → task3)或分层(经理智能体委派任务)。适合线性或简单委派任务(Notebook 示例中可见)。
- LangGraph 提供完整、细粒度控制:因为工作流是图,你可以自然地构建循环、分支与回路;可用条件边表达“若校验失败则走拒绝节点,否则走征信检查节点”。这种错误处理与循环能力是许多高级智能体模式的关键需求。
- ADK 在两者间平衡:既能运行确定性的工作流(如 SequentialAgent),也允许动态的、LLM 驱动的规划——由智能体决定下一步,再交由运行时(agent runtime)管理执行。
状态管理(State management)
- LangGraph 的核心优势是显式状态管理:你定义一个 State 对象(如 dict 或 TypedDict)承载全部信息;该 State 传递给每个节点;节点执行后返回对 State 的更新。这使调试更容易,因为可以逐步检查状态。
- CrewAI 的状态管理更隐式:一个任务的输出会被自动格式化并作为上下文传递给依赖它的后续任务。简单链路很快,但相对缺乏 LangGraph 那样的直接控制与可检查性。
- ADK 使用托管状态:运行时与会话服务负责跨交互持久化智能体状态与记忆,抽象掉复杂性,并确保长时间运行的智能体也能从中断处继续。
现在我们对这些框架有了更深入理解,接下来退一步并排比较它们。
对比分析:ADK、CrewAI 与 LangGraph(Comparative analysis: ADK, CrewAI, and LangGraph)
在分别了解每个框架后,我们来看它们并排对比的结果。该分析将帮助你基于项目的成熟度、复杂度与运营需求选择合适工具。
表 15.1 —— 三种框架的生态与理念概览(高层对比)
| Framework | Primary supporter | Announced/launched | Core philosophy and focus |
|---|---|---|---|
| Google ADK | 2024(prototype)/2025(public) | 一个全面的开源工具包,用于构建、评估与部署生产级、鲁棒的智能体。针对 Google 生态(Gemini 与 Vertex AI)优化,但设计为模型无关(model-agnostic)。 | |
| CrewAI | CrewAI(founded by João Moura) | 2023(open source launch) | 用于编排“角色扮演”的自主 AI 智能体框架。强调协作智能:智能体作为一个“crew”协同完成目标。 |
| LangGraph | LangChain | 2024 | LangChain 的扩展,用于构建有状态、多参与者应用。通过将流程建模为图(状态机)来擅长实现循环过程与复杂控制流。 |
表 15.2 —— 架构/控制流/适用阶段的深入对比(技术向)
| Feature | Google ADK | CrewAI | LangGraph |
|---|---|---|---|
| Core abstraction | 生产级智能体与运行时(runtimes)。 | 角色扮演团队(“crew”)。 | 有状态图(“state machine”)。 |
| Control flow | 规划器驱动;由运行时管理。可顺序或并行。 | 高层流程(顺序或分层)。 | 细粒度;由图的边定义。非常适合循环与分支。 |
| State management | 托管:由会话(session)与运行时处理。 | 隐式:任务间自动通过上下文传递。 | 显式:中心 State 对象传入并由每个节点更新。 |
| Callbacks and hooks | 中间件/拦截器模式:回调作为“护栏”,在 LLM 调用前后拦截 I/O。关键能力:在线修改数据(如 PII 脱敏)或完全绕过 LLM(缓存)。 | 事件驱动 hooks:在生命周期事件触发(如 on_task_start、on_task_end)。关键能力:可观测性与副作用(日志、更新 UI、触发 webhook),不改变核心智能体逻辑。 | 状态监听与 interrupts:回调用于追踪(LangSmith),控制更多依赖“中断”。关键能力:在特定节点暂停图(checkpoint)等待 HITL 输入后恢复。 |
| Best for... | 生产系统、企业集成(尤其 Google Cloud)、鲁棒且可测试的智能体。 | 快速原型验证协作任务、角色定义工作流(如“researcher”“writer”)。 | 复杂动态工作流;显式错误处理;循环;HITL。 |
接下来,为了更直观理解这些差异,我们将基于开发 Notebook 的具体代码,使用 CrewAI 与 LangGraph 重新实现第 13 章的贷款处理用例。
重新实现贷款智能体:一次实践对比(Re-implementing the loan agent: A practical comparison)
为了更具体地展示这些框架之间的差异,我们现在将基于配套 Notebook 中的代码,实现一个多智能体贷款处理系统。目标仍然是:接收 applicant_id 和 document_id,抓取文档内容,并产出一个最终、可审计的贷款决策。
该工作流包含若干相互独立的任务。下面,我们把每个任务映射到两种实现中负责处理它的具体组件:
- 文档抓取(Document fetch) :获取贷款申请文档的内容(LangGraph:
node_fetch_document| CrewAI:作为初始输入/预处理传入)。 - 文档校验(Document validation) :检查抓取到的文档内容是否有效且完整(LangGraph:
node_validate_document| CrewAI:文档校验专家)。 - 征信查询(Credit check) :基于
customer_id获取借款人的信用分(LangGraph:node_check_credit| CrewAI:征信查询智能体)。 - 风险评估(Risk assessment) :综合文档状态、信用分与收入,确定风险等级(LangGraph:
node_assess_risk| CrewAI:风险评估分析师)。 - 合规检查(Compliance check) :确保最终决策符合放贷监管要求(LangGraph:
node_check_compliance| CrewAI:合规官)。
我们将分别用 CrewAI 和 LangGraph 构建这一工作流,以突出它们在使用 Google Gemini 作为 LLM 时的不同实现思路。
无论使用哪种框架,智能体与外部系统交互的能力都由其**工具(tools)**定义。对于两种实现,我们都先定义核心业务逻辑。在提供的 Notebook 中,这些逻辑被定义为继承自 CrewAI 的 BaseTool 的 Python 类:
# --- Define Tool Classes inheriting from BaseTool ---
import json
from crewai.tools import BaseTool
class ValidateDocumentFieldsTool(BaseTool):
name: str = "Validate Document Fields"
description: str = (
"Validates that the loan application JSON string contains the required fields: " "'customer_id', 'loan_amount', 'income', and 'credit_history'."
)
def _run(self, application_data: str) -> str:
"""Validates the application data."""
print(f"--- TOOL: Validating document fields ---")
try:
data = json.loads(application_data)
required_fields = ["customer_id", "loan_amount", "income", "credit_history"]
missing_fields = [field for field in required_fields if field not in data]
if missing_fields:
return json.dumps({"error": f"Validation failed: Missing required fields: {', '.join(missing_fields)}"})
# Return the original data if valid
return json.dumps({"status": "validated", "data": data})
except json.JSONDecodeError:
return json.dumps({"error": "Invalid JSON format in application data."})
class QueryCreditBureauAPITool(BaseTool):
name: str = "Query Credit Bureau API"
description: str = (
"Simulates a call to a credit bureau API to retrieve a credit score given a customer_id."
)
def _run(self, customer_id: str) -> str:
"""Queries the mock credit bureau."""
print(f"--- TOOL: Calling Credit Bureau API for customer: {customer_id} ---")
mock_credit_scores = {
"CUST-12345": 810, # Happy Path
"CUST-55555": 620, # High Risk Path
"borrower_good_780": 810,
"borrower_bad_620": 620
}
score = mock_credit_scores.get(customer_id)
if score is not None:
return json.dumps({"customer_id": customer_id, "credit_score": score})
return json.dumps({"error": "Customer ID not found."})
class CalculateRiskScoreTool(BaseTool):
name: str = "Calculate Risk Score"
description: str = (
"Calculates a risk score based on loan_amount, income, and credit_score."
)
def _run(self, loan_amount: int, income: str, credit_score: int) -> str:
"""Calculates the risk score."""
print(f"--- TOOL: Calculating risk score ---")
try:
# Attempt to parse income string (e.g., "USD 120000 a year", "$60k/month")
income_value = int(''.join(filter(str.isdigit, income)))
annual_income = income_value * 12 if "month" in income.lower() else income_value
except (ValueError, TypeError):
annual_income = 0 # Default to 0 if income cannot be parsed
if annual_income == 0:
risk_score = 10 # Assign highest risk if income is zero or invalid
else:
loan_to_income_ratio = loan_amount / annual_income
risk_score = 1 # Start with base risk
if credit_score < 650: risk_score += 4
elif credit_score < 720: risk_score += 2
if loan_to_income_ratio > 0.8: risk_score += 5
elif loan_to_income_ratio > 0.5: risk_score += 2
# Cap risk score at 10
return json.dumps({"risk_score": min(risk_score, 10)})
class CheckLendingComplianceTool(BaseTool):
name: str = "Check Lending Compliance"
description: str = (
"Checks the application against internal policies using credit_history and risk_score."
)
def _run(self, credit_history: str, risk_score: int) -> str:
"""Checks compliance rules."""
print(f"--- TOOL: Checking compliance rules (including risk score) ---")
if credit_history == "No History":
return json.dumps({"is_compliant": False, "reason": "Policy violation: No credit history is an automatic denial."})
if risk_score >= 8: # Risk score of 8 or higher is non-compliant
return json.dumps({"is_compliant": False, "reason": f"Policy violation: Risk score of {risk_score} is too high for approval."})
return json.dumps({"is_compliant": True, "reason": "Application meets all internal policy guidelines."})
# --- Instantiate the Tools ---
validate_document_fields_tool = ValidateDocumentFieldsTool()
query_credit_bureau_api_tool = QueryCreditBureauAPITool()
calculate_risk_score_tool = CalculateRiskScoreTool()
check_lending_compliance_tool = CheckLendingComplianceTool()
我们还需要一个辅助函数,用于模拟根据 ID 抓取文档内容:
# --- Helper Function for Mock Data ---
import json
def get_document_content(document_id: str) -> str:
"""
Simulates fetching document content based on its ID.
Returns a JSON STRING.
"""
print(f"--- HELPER: Simulating fetch for doc_id: {document_id} ---")
if document_id == "document_valid_123":
data = {
"customer_id": "CUST-12345",
"loan_amount": 50000,
"income": "USD 120000 a year",
"credit_history": "7 years"
}
return json.dumps(data)
elif document_id == "document_invalid_456":
data = {
"customer_id": "CUST-55555",
"loan_amount": 200000,
# "income" is missing
"credit_history": "1 year"
}
return json.dumps(data)
else:
return json.dumps({"error": "Document ID not found."})
关于工具输出模式的说明(Note on tool output patterns)
你会注意到,这些工具返回的是 JSON 编码的字符串,而不是 Python 字典。这是智能体系统中的一个刻意设计选择:因为工具输出的主要消费者往往是 LLM 本身(它处理的是文本 token),返回显式的 JSON 字符串可以确保模型拿到一个结构化、可读、易解析或易推理的格式。
此外,我们在这里建立了一个数据契约(data contract) :工具承诺在成功时返回 {"status": "validated", "data": ...} 结构,在失败时返回 {"error": ...} 结构。这种一致性使得下游智能体(或 CheckLendingCompliance 工具)能够以确定性的方式处理错误。
生产建议:数据归一化模式(Production tip: Data normalization patterns)
在这个示例里,CalculateRiskScoreTool 通过简单的字符串解析来提取收入数字。在生产环境中,这种方式过于脆弱。你应该在上游实现一个专用的归一化节点或预处理工具,负责货币换算、地区格式处理(例如 "$100k" vs "100,000 EUR")以及标准化,然后再把数据交给“逻辑偏重”的风险评估智能体。
生产模式——代理工具(Production pattern – the proxy tool / Agent Calls Proxy Agent)
前面的 if/else 逻辑是为了演示而简化的启发式规则。在真实的企业架构中,这个工具会以“代理(proxy)”的方式工作(参见第 8 章的 Agent Calls Proxy Agent 模式)。
此时 _run 方法会充当一个包装器:构造对外部风险决策引擎或已部署的机器学习模型端点的安全 API 请求,执行调用,并解析响应。该模式能让智能体保持轻量,同时确保关键业务逻辑仍然集中、可治理。
现在,让我们看看 CrewAI 与 LangGraph 如何编排同一套工具。
实现 1:CrewAI(协作团队)(Implementation 1: CrewAI (the collaborative team))
Notebook 使用 CrewAI 的分层(hierarchical)流程来实现:由一个经理智能体把任务委派给多个专精智能体。
定义 LLM 与智能体(Define the LLM and agents)
首先,我们通过 CrewAI 的 LLM 抽象配置 Gemini。然后定义专精智能体(每个分配特定工具)以及经理智能体(无工具,但可以委派):
import os
import json
from crewai import Agent, Task, Crew, Process, LLM
from crewai.tools import BaseTool
# Assume tools (ValidateDocumentFieldsTool, etc.) and get_document_content are defined above
# --- Initialize the LLM ---
# Assumes GOOGLE_API_KEY environment variable is set
llm = LLM(
model='gemini/gemini-2.5-flash', # Or another Gemini model
api_key=os.getenv("GOOGLE_API_KEY"),
temperature=0.0
)
# --- Define Agents ---
# 1. Document Validation Agent
doc_specialist = Agent(
role="Document Validation Specialist",
goal="Validate the completeness and format of a new loan application provided as a JSON string.",
backstory=(
"You are a meticulous agent responsible for the first step of loan processing. "
"Your sole task is to receive a JSON string, call the `Validate Document Fields` tool, "
"and return its exact JSON output. You do not talk to the user or other agents."
),
tools=[validate_document_fields_tool],
llm=llm,
allow_delegation=False,
verbose=True
)
# 2. Credit Check Agent
credit_analyst = Agent(
role="Credit Check Agent",
goal="Query the credit bureau API to retrieve an applicant's credit score.",
backstory=(
"You are a specialized agent that interacts with the Credit Bureau. "
"Your sole task is to receive a `customer_id`, call the `Query Credit Bureau API` tool, "
"and return its exact JSON output."
),
tools=[query_credit_bureau_api_tool],
llm=llm,
allow_delegation=False,
verbose=True
)
# 3. Risk Assessment Agent
risk_assessor = Agent(
role="Risk Assessment Analyst",
goal="Calculate the financial risk score for a loan application.",
backstory=(
"You are a quantitative analyst agent. Your sole task is to receive `loan_amount`, `income`, and `credit_score`, "
"call the `Calculate Risk Score` tool, and return its exact JSON output."
),
tools=[calculate_risk_score_tool],
llm=llm,
allow_delegation=False,
verbose=True
)
# 4. Compliance Agent
compliance_officer = Agent(
role="Compliance Officer",
goal="Check the application against all internal lending policies and compliance rules.",
backstory=(
"You are the final checkpoint for policy and compliance. Your sole task is to receive `credit_history` and `risk_score`, "
"call the `CheckLendingCompliance` tool, and return its exact JSON output."
),
tools=[check_lending_compliance_tool],
llm=llm,
allow_delegation=False,
verbose=True
)
# 5. Manager Agent (for the final report)
manager = Agent(
role="Loan Processing Manager",
goal="Manage the loan application workflow and compile the final report.",
backstory=(
"You are the manager responsible for orchestrating the "
"loan processing pipeline, ensuring data flows correctly, and formulating the "
"final decision and report based on your team's findings."
),
llm=llm,
allow_delegation=True, # The manager delegates tasks
verbose=True
)
通过 temperature 降低方差(Minimizing variance with temperature)
我们刻意将 temperature=0.0 用于该智能体。对于依赖精确工具调用与结构化输出(如 JSON)的智能体工作流而言,尽可能降低随机性至关重要。需要注意的是,即便 temperature=0.0 能显著降低方差,也无法保证 100% 的确定性行为,因为 GPU 上浮点运算本身存在非确定性。但它仍能为逻辑与编排任务提供尽可能高的稳定性。
定义任务(Define the tasks)
接下来,我们定义任务。注意 task_validate 的描述里包含占位符 {document_content},该占位符会接收抓取到的 JSON 字符串作为输入。context 参数会在依赖任务之间隐式传递输出:
# Define input document IDs for testing
loan_application_doc_ids = {
"valid": "document_valid_123",
"invalid": "document_invalid_456"
}
# Task 1: Validate Document Content
task_validate = Task(
description=(
"Validate the loan application, which is provided as a JSON string: '{document_content}'. "
"You MUST pass this entire JSON string directly to the 'Validate Document Fields' tool."
),
expected_output="A JSON string with the validation status and all extracted data ('status': '...', 'data': {...}) or an error message.",
# Agent is not assigned here; manager will delegate
)
# Task 2: Check Credit
task_credit = Task(
description=(
"1. Parse the JSON output from the validation task. \n"
"2. Extract the `customer_id` from its 'data' field. \n"
"3. Call the `Query Credit Bureau API` tool with this `customer_id`."
),
expected_output="A JSON string containing the customer_id and their credit_score.",
context=[task_validate] # Depends on task_validate
)
# Task 3: Assess Risk
task_risk = Task(
description=(
"1. Parse the JSON output from the validation task to get `loan_amount` and `income`. \n"
"2. Parse the JSON output from the credit check task to get `credit_score`. \n"
"3. Call the `Calculate Risk Score` tool with these three values."
),
expected_output="A JSON string containing the calculated risk_score.",
context=[task_validate, task_credit] # Depends on two tasks
)
# Task 4: Check Compliance
task_compliance = Task(
description=(
"1. Parse the JSON output from the validation task to get `credit_history`. \n"
"2. Parse the JSON output from the risk assessment task to get `risk_score`. \n"
"3. Call the `Check Lending Compliance` tool with these two values."
),
expected_output="A JSON string with the compliance status (is_compliant: true/false) and a reason.",
context=[task_validate, task_risk] # Depends on two tasks
)
# Task 5: Compile Final Report
task_report = Task(
description=(
"Compile a final loan decision report synthesizing all findings from the previous tasks. "
"The report must include: \n"
"- The final decision (Approve/Deny). \n"
"- A clear justification for the decision, referencing the validation status, "
"credit score, risk score, and compliance check."
),
expected_output="A comprehensive final report in Markdown format.",
context=[task_validate, task_credit, task_risk, task_compliance] # Depends on all tasks
)
组装并运行 crew(Assemble and run the crew)
最后,我们组装 Crew:指定分层流程,并设置 manager_agent。在启动 crew 之前先抓取文档内容,并作为输入传入:
# Assemble the crew
loan_crew = Crew(
agents=[doc_specialist, credit_analyst, risk_assessor, compliance_officer], # Manager assigned below
tasks=[task_validate, task_credit, task_risk, task_compliance, task_report],
process=Process.hierarchical,
manager_agent=manager,
verbose=True
)
# --- Run with VALID inputs ---
print("--- KICKING OFF CREWAI PROCESS (VALID INPUTS) ---")
valid_json_content = get_document_content(loan_application_doc_ids['valid'])
inputs_valid = {'document_content': valid_json_content}
result_valid = loan_crew.kickoff(inputs=inputs_valid)
print("\n\n--- CREWAI FINAL REPORT (VALID) ---")
print(result_valid)
# --- Run with INVALID inputs ---
print("\n\n--- KICKING OFF CREWAI PROCESS (INVALID INPUTS) ---")
invalid_json_content = get_document_content(loan_application_doc_ids['invalid'])
inputs_invalid = {'document_content': invalid_json_content}
result_invalid = loan_crew.kickoff(inputs=inputs_invalid)
print("\n\n--- CREWAI FINAL REPORT (INVALID) ---")
print(result_invalid)
CrewAI 的分层方式使经理智能体能够编排整个工作流:经理会根据任务描述与可用工具,把每个任务委派给合适的专精智能体。错误处理在某种程度上是隐式的:如果 task_validate 返回错误(例如无效用例中缺少 income 字段),后续依赖其输出的任务仍可能继续执行,但很可能失败或产出不正确结果,因为经理会尝试推进流程。无效用例的最终报告会体现校验失败,但中间步骤(征信查询、风险评估)仍然会被执行,从而可能产生不必要的动作。
实现 2:LangGraph(状态机)(Implementation 2: LangGraph (the state machine))
LangGraph 的实现采用显式的状态机。我们为每一步定义节点(包括抓取文档),并用条件边(conditional edges) 实现更鲁棒的错误处理。
定义状态(Define the state)
我们使用 TypedDict 定义 LoanGraphState,包含整个流程中工具与节点所需的全部字段:
#@title 2.1: Define LangGraph State
import typing
import json
class LoanGraphState(typing.TypedDict):
"""
Represents the state of our loan processing graph.
It contains all the data that needs to be passed between nodes.
"""
applicant_id: str # Initial input, may not be directly used if customer_id is in doc
document_id: str # Initial input
document_content: str # Fetched content (JSON string)
# Data extracted or generated by tools/nodes
validation_status: str
customer_id: str
loan_amount: int
income: str
credit_history: str
credit_score: int
risk_score: int
risk_level: str # Added for LLM-based risk assessment output
compliance_status: str
# Final output
final_decision: str # Simplified final report/decision string
error: str # To track errors explicitly
定义图节点(Define the graph nodes)
我们用 Python 函数作为节点。node_fetch_document 模拟抓取内容;node_validate_document 调用校验工具,并用抽取出的数据或错误来更新状态。后续节点在继续之前都会检查是否存在错误。node_assess_risk 则直接使用 LLM 生成风险评估:
#@title 2.2: Define LangGraph Nodes
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Re-initialize LLM specifically for LangGraph (using LangChain's integration)
lg_llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash") # Or your preferred Gemini model
# Node 0: Fetch Document Content
def node_fetch_document(state: LoanGraphState):
print("--- NODE: Fetching Document ---")
doc_id = state["document_id"]
try:
content = get_document_content(doc_id)
# Check if the helper returned an error (e.g., document not found)
content_json = json.loads(content)
if "error" in content_json:
print(f" Error fetching document: {content_json['error']}")
return {
"error": f"Failed to fetch document {doc_id}: {content_json['error']}",
"document_content": ""
}
return {"document_content": content}
except Exception as e:
print(f" Error during document fetch node: {e}")
return {"error": f"Critical error fetching document {doc_id}", "document_content": ""}
# Node 1: Validate Document
def node_validate_document(state: LoanGraphState):
print("--- NODE: Validating Document ---")
# Check if fetch already failed
if state.get("error"):
return {"validation_status": "SKIPPED due to fetch error"}
doc_content = state["document_content"]
try:
result_str = validate_document_fields_tool._run(application_data=doc_content)
result_json = json.loads(result_str)
if "error" in result_json:
validation_status = f"Validation FAILED: {result_json['error']}"
print(f" -> {validation_status}")
# Explicitly set error state
return {"validation_status": validation_status, "error": validation_status}
else:
validation_status = result_json.get('status', 'Validation PASSED')
app_data = result_json.get("data", {})
print(f" -> {validation_status}")
# Update state with extracted data
return {
"validation_status": validation_status,
"customer_id": app_data.get("customer_id"),
"loan_amount": app_data.get("loan_amount"),
"income": app_data.get("income"),
"credit_history": app_data.get("credit_history"),
"error": None # Clear any previous error if validation succeeds
}
except Exception as e:
validation_status = f"Critical error during validation node: {e}"
print(f" -> {validation_status}")
return {"validation_status": validation_status, "error": validation_status}
# Node 2: Check Credit
def node_check_credit(state: LoanGraphState):
print("--- NODE: Checking Credit ---")
if state.get("error"): # Skip if validation failed
return {"credit_score": -1}
cust_id = state["customer_id"]
try:
result_str = query_credit_bureau_api_tool._run(customer_id=cust_id)
result_json = json.loads(result_str)
if "error" in result_json:
print(f" -> Error: {result_json['error']}")
# Set error state if credit check fails
return {"credit_score": -1, "error": f"Credit check failed: {result_json['error']}"}
score = result_json.get("credit_score", -1)
print(f" -> Credit Score: {score}")
return {"credit_score": score, "error": None} # Clear error on success
except Exception as e:
print(f" Critical error during credit check node: {e}")
return {"error": "Critical error in credit check tool.", "credit_score": -1}
# Node 3: Assess Risk (LLM-Powered)
def node_assess_risk(state: LoanGraphState):
print("--- NODE: Assessing Risk (LLM-Powered) ---")
if state.get("error"):
return {"risk_score": -1, "risk_level": "UNKNOWN"}
prompt = ChatPromptTemplate.from_template(
"""You are a senior loan underwriter. Assess the financial risk based on:
- Validation Status: {validation}
- Credit Score: {credit}
- Loan Amount: {amount}
- Applicant Income: {income}
- Credit History: {history}
Provide a one-sentence justification, then conclude with the risk level: LOW, MEDIUM, or HIGH.
Example:
Justification: Applicant has excellent credit and low debt-to-income.
Risk: LOW
"""
)
parser = StrOutputParser()
risk_chain = prompt | lg_llm | parser
try:
result_str = risk_chain.invoke({
"validation": state.get("validation_status", "N/A"),
"credit": state.get("credit_score", "N/A"),
"amount": state.get("loan_amount", "N/A"),
"income": state.get("income", "N/A"),
"history": state.get("credit_history", "N/A")
})
print(f" -> LLM Assessment Output:\n{result_str}")
# Basic parsing
risk_level = "UNKNOWN"
if "LOW" in result_str.upper(): risk_level = "LOW"
elif "MEDIUM" in result_str.upper(): risk_level = "MEDIUM"
elif "HIGH" in result_str.upper(): risk_level = "HIGH"
score_map = {"LOW": 3, "MEDIUM": 6, "HIGH": 9, "UNKNOWN": 10}
risk_score = score_map.get(risk_level, 10)
print(f" -> Parsed Risk Level: {risk_level}, Score: {risk_score}")
return {"risk_score": risk_score, "risk_level": risk_level, "error": None}
except Exception as e:
print(f" Critical error during LLM risk assessment node: {e}")
return {"error": "Critical error in LLM Risk assessment.", "risk_score": -1, "risk_level": "UNKNOWN"}
# Node 4: Check Compliance
def node_check_compliance(state: LoanGraphState):
print("--- NODE: Checking Compliance ---")
if state.get("error"):
return {"compliance_status": "SKIPPED due to prior error."}
try:
result_str = check_lending_compliance_tool._run(
credit_history=state["credit_history"],
risk_score=state["risk_score"]
)
result_json = json.loads(result_str)
status = result_json.get("reason", "Check FAILED")
print(f" -> Compliance Status: {status}")
# Set error if non-compliant, otherwise clear it
error_msg = status if result_json.get("is_compliant") is False else None
return {"compliance_status": status, "error": error_msg}
except Exception as e:
print(f" Critical error during compliance check node: {e}")
return {"error": "Critical error in compliance check tool.", "compliance_status": "Check FAILED due to tool error."}
# Node 5: Compile Final Report (Handles success path)
def node_compile_report(state: LoanGraphState):
print("--- NODE: Compiling Success Report ---")
# This node is only reached if all previous steps succeeded without setting the error state
decision = "Approve"
reason = (f"Approved based on:\n"
f" - Validation: {state.get('validation_status', 'N/A')}\n"
f" - Credit Score: {state.get('credit_score', 'N/A')}\n"
f" - Risk Assessment: {state.get('risk_level', 'N/A')} (Score: {state.get('risk_score', 'N/A')})\n"
f" - Compliance: {state.get('compliance_status', 'N/A')}")
report = f"FINAL DECISION: {decision}\nREASON: {reason}"
return {"final_decision": report.strip()}
# Node 6: Compile Rejection Report (Handles any failure path)
def node_compile_rejection(state: LoanGraphState):
print("--- NODE: Compiling Rejection Report ---")
decision = "Deny"
reason = f"Denied due to error: {state.get('error', 'Unknown error during processing.')}"
# Add more specific reasons based on which stage failed if needed
if "Validation FAILED" in state.get("validation_status", ""):
reason = f"Denied due to validation failure: {state.get('validation_status', '')}"
elif "Credit check failed" in state.get("error", ""):
reason = f"Denied due to credit check failure: {state.get('error', '')}"
elif "compliance" in state.get("error", "").lower(): # Check if compliance node set the error
reason = f"Denied due to compliance failure: {state.get('compliance_status', '')}"
report = f"FINAL DECISION: {decision}\nREASON: {reason}"
return {"final_decision": report.strip()}
生产建议:强制结构化输出(Production tip: Enforcing structured output)
在这个例子里,我们用简单的字符串匹配来解析 LLM 的响应。生产系统中这很危险,因为模型可能更啰嗦(例如输出 “The risk is relatively LOW”)。对于企业级应用,你应使用结构化输出能力(LangChain 与 Gemini 都支持)。通过向模型传入 Pydantic schema,你可以强制其返回合法 JSON 对象(例如 {"risk_level": "LOW"}),从而保证输出满足下游要求,无需依赖脆弱的字符串解析逻辑。
定义图及其边(Define the graph and its edges)
我们从 fetch_doc 开始把节点连起来。关键点在于:在 fetch_doc 与 validate_doc 之后加入条件边,检查 state 中的 error 字段;一旦出现错误,立即路由到 compile_rejection,实现快速失败:
#@title 2.3: Define and Compile the Graph
from langgraph.graph import StateGraph, END
workflow = StateGraph(LoanGraphState)
# Add nodes
workflow.add_node("fetch_doc", node_fetch_document)
workflow.add_node("validate_doc", node_validate_document)
workflow.add_node("check_credit", node_check_credit)
workflow.add_node("assess_risk", node_assess_risk)
workflow.add_node("check_compliance", node_check_compliance)
workflow.add_node("compile_report", node_compile_report) # Success end node
workflow.add_node("compile_rejection", node_compile_rejection) # Failure end node
# Set entry point
workflow.set_entry_point("fetch_doc")
# Define conditional edge logic
def decide_after_fetch(state: LoanGraphState):
return "reject" if state.get("error") else "continue"
def decide_after_validation(state: LoanGraphState):
return "reject" if state.get("error") else "continue"
def decide_after_credit_check(state: LoanGraphState):
return "reject" if state.get("error") else "continue"
def decide_after_risk(state: LoanGraphState):
# Even if risk is HIGH, we proceed to compliance check,
# but compliance node might set error state.
return "reject" if state.get("error") else "continue"
def decide_after_compliance(state: LoanGraphState):
# If compliance node set an error (e.g., non-compliant), reject.
return "reject" if state.get("error") else "continue"
# Add edges
workflow.add_conditional_edges("fetch_doc", decide_after_fetch, {"continue": "validate_doc", "reject": "compile_rejection"})
workflow.add_conditional_edges("validate_doc", decide_after_validation, {"continue": "check_credit", "reject": "compile_rejection"})
workflow.add_conditional_edges("check_credit", decide_after_credit_check, {"continue": "assess_risk", "reject": "compile_rejection"})
workflow.add_conditional_edges("assess_risk", decide_after_risk, {"continue": "check_compliance", "reject": "compile_rejection"})
workflow.add_conditional_edges("check_compliance", decide_after_compliance, {"continue": "compile_report", "reject": "compile_rejection"})
# Define end points
workflow.add_edge("compile_report", END)
workflow.add_edge("compile_rejection", END)
# Compile
try:
app = workflow.compile()
print("LangGraph Compiled Successfully!")
# Optional: Visualize
# from IPython.display import Image, display
# display(Image(app.get_graph().draw_mermaid_png()))
except Exception as e:
print(f"Error compiling LangGraph: {e}")
app = None
运行图(Run the graph)
我们通过 .stream() 运行已编译的图(app),以观察在有效与无效文档 ID 下的状态迁移:
#@title 2.4: Run the LangGraph Workflow
if app is None:
print("LangGraph app not compiled. Skipping execution.")
else:
# --- Test 1: Valid Data ---
print("\n--- LANGGRAPH RUN 1: VALID DOCUMENT ---")
inputs_valid = {
"applicant_id": "borrower_good_780", # Included but might not be used if customer_id is preferred
"document_id": "document_valid_123",
}
print("Streaming intermediate steps (Valid):")
for s_chunk in app.stream(inputs_valid, {"recursion_limit": 10}):
step_name = list(s_chunk.keys())[0]
print(f" Step: {step_name}") # Simpler logging
# print(f" Output: {s_chunk[step_name]}") # Uncomment for full state change detail
print("-" * 10)
print("\nInvoking for final state (Valid)...")
final_state_valid = app.invoke(inputs_valid, {"recursion_limit": 10})
print("\n--- LANGGRAPH FINAL REPORT (VALID) ---")
print(final_state_valid.get('final_decision', 'Final decision not found.'))
# print("\nFull Final State (Valid):", final_state_valid) # Uncomment to see full state
# --- Test 2: Invalid Data ---
print("\n\n--- LANGGRAPH RUN 2: INVALID DOCUMENT ---")
inputs_invalid = {
"applicant_id": "borrower_bad_620",
"document_id": "document_invalid_456",
}
print("Streaming intermediate steps (Invalid):")
for s_chunk in app.stream(inputs_invalid, {"recursion_limit": 10}):
step_name = list(s_chunk.keys())[0]
print(f" Step: {step_name}")
# print(f" Output: {s_chunk[step_name]}")
print("-" * 10)
print("\nInvoking for final state (Invalid)...")
final_state_invalid = app.invoke(inputs_invalid, {"recursion_limit": 10})
print("\n--- LANGGRAPH FINAL REPORT (INVALID) ---")
print(final_state_invalid.get('final_decision', 'Final decision not found.'))
# print("\nFull Final State (Invalid):", final_state_invalid)
这个 LangGraph 实现展示了显式的控制流与状态管理。加入 node_fetch_document 让流程从一开始就更“干净”。基于 state 中 error 字段的条件边确保:一旦抓取或校验失败,图会立刻路由到 compile_rejection 节点,从而避免在无效数据上继续进行不必要的工具调用(例如征信查询或风险评估)。
这种显式路由带来可量化的效率收益:在失败发生时立即停止执行,减少不必要的 API 成本与延迟——这与我们的 CrewAI 示例形成直接对照:在 CrewAI 中,即便初始校验出错,中间智能体仍可能继续运行。
node_assess_risk 中直接使用 LLM 的做法也展示了 LangGraph 如何把生成式步骤与确定性的工具调用并排集成在同一工作流里。对于这个特定流程而言,基于图的方式在鲁棒性与可追踪性上优于更简单的 CrewAI 流程,尤其体现在错误处理方面。
接下来,我们将进入可观测性与负责任 AI 的注意事项。
可观测性与负责任 AI 的考量(Observability and responsible AI considerations)
选择一个框架不只是开发体验的问题;更关键的是你是否具备管理、监控与治理最终应用的能力。在智能体 AI 的语境下,由于**非确定性(non-determinism)**客观存在,**可观测性(observability)**就是负责任 AI 的基石:如果你无法追溯一个智能体为何做出某个决策,你就无法确保它是公平的、安全的或合规的。
实践中的可观测性(Observability in practice)
每个框架都会借助特定工具与协议,为调试复杂的智能体交互、并维护可验证的审计轨迹(audit trail)提供所需的可见性:
- LangGraph 与 CrewAI(结合 LangSmith) :LangChain 生态(包括 LangGraph 与 CrewAI)被设计为能够原生集成 LangSmith。LangSmith 是一个专门用于追踪复杂 LLM 应用的可观测性平台。由于 LangGraph 的状态是显式的,它在 LangSmith 中的 trace 极其细致,使你能通过查看每个节点的完整状态与 LLM 调用来实现“时间旅行式(time-travel)”调试。CrewAI 的 trace 同样能从 LangSmith 中受益:它会展示智能体的动作与工具调用,从而提供智能体“思考与行动”的完整审计轨迹,这对调试与可解释性而言非常关键。
- Google ADK:作为面向生产的工具包,ADK 使用 OpenTelemetry(行业标准的追踪与指标体系)进行埋点。这使其能直接与企业级监控方案集成,例如 Google Cloud 的运维套件(Cloud Trace 与 Cloud Logging)。这种方式让智能体更像一个可管理的微服务,而不是一个脚本,这对企业治理至关重要。
促进负责任 AI(Enabling responsible AI)
负责任 AI 的原则包括:公平(fairness) 、透明(transparency) 、可问责(accountability)与安全(safety) 。这些不是抽象口号,而是由具体架构选择所支撑,并通过组织层面的持续治理与监督来落地——通过强制纳入以下原则与护栏(guardrails)来实现:
透明与可解释性(Transparency and explainability)
LangGraph 的显式状态图本身就是一种可解释性:图记录了决策逻辑,而最终的 State 对象包含了达成结论所用到的全部中间数据。我们贷款智能体的最终报告(包含决策理由)就是这一可追溯过程的直接产物:
- 人口统计平等(demographic parity)测试场景:在 CI/CD 流水线中借助 ADK 的测试框架加入公平性评估步骤。它会用来自不同人口统计群体、经过筛选的“黄金数据集(golden dataset)”用户查询来运行智能体,并在版本晋升前以数学方式衡量响应质量(例如帮助性、语气)是否在不同用户群体间保持一致。
- 推理透明场景(reasoning transparency) :使用 Google Cloud 控制台中的 Trace 视图(链接到 ADK 部署)。它会暴露智能体内部的“思考—行动—观察(thought, action, observation)”循环,让开发者看到智能体为何选择调用
getUserBalance工具而不是getLoanStatus工具,而不只是看到最终答案。 - 显式状态图可视化场景(explicit state graph visualization) :在部署贷款智能体之前,用多样化申请人画像构成的“黄金数据集”跑一遍模型,以验证审批逻辑不会基于受保护属性(例如邮编或性别)产生差别影响(disparate impact),即使这些属性并未被显式作为特征输入。
安全与鲁棒性(Safety and robustness)
在我们的 LangGraph 实现中,条件边(conditional edges)通过强制“快速失败(fail-fast)”逻辑,构成一种程序化的安全模式:图会监控 state 对象中的专用 error 键;一旦检测到错误(例如校验阶段发现缺失 income 字段),条件边会立刻把执行流重路由到终止的拒绝节点。这会阻止下游智能体继续处理无效输入,从而显著降低模型在错误信息上“幻觉式”做出决策,或基于错误数据执行未授权 API 调用的风险。
- 安全护栏与 PII 保护场景:在 ADK 的模型参数中显式配置
safety_settings,以BLOCK_LOW_AND_ABOVE阈值阻断“仇恨言论(hate speech)”或“骚扰(harassment)”。同时实现特定输入护栏(如 PII 检测),在敏感数据进入 LLM 上下文窗口之前进行拦截与脱敏(redact)。 - 条件边护栏场景:在图中硬编码一个停止条件(条件边):如果收入核验 API 返回
null或负值,立即终止流程,防止 LLM 在坏数据上幻觉出授信决策。
可问责与治理(Accountability and governance)
一个可追溯、可观测的工作流是实现可问责的前提。当审计人员询问“为什么某笔贷款被拒绝”时,你可以提供 LangSmith 或 Google Cloud Trace 的完整 trace,逐步展示每一步使用的精确数据、工具输出与 LLM 推理(尤其是在 LangGraph 的显式状态下)。这会把智能体从“黑盒”变成业务流程中透明、可审计的组件:
- 不可变审计轨迹场景(immutable audit trail) :启用数据访问日志,并将所有智能体交互日志导出到 BigQuery。这样会形成不可变记录:智能体发出的每次 API 调用都带有时间戳,并关联到特定的服务账号身份,使合规团队可以查询“何时、由谁授权了某笔交易”。
- 完整执行 trace(LangSmith/Cloud Trace)场景:当审计对某个拒绝案例提出质疑时,从 LangSmith 中检索对应 trace ID,里面会记录具体 prompt、检索到的信用分,以及导致 Denied 输出的 LLM 中间推理步骤。
既然我们已经用更新后的代码看到了这些框架如何工作,接下来讨论如何为你的项目选择合适的框架。
选择框架的建议(Recommendations for choosing a framework)
不存在唯一的“最佳”智能体框架。正确选择取决于项目复杂度、团队对概念的熟悉程度以及生产环境要求。不过,一个关键的长期策略是避免框架锁定(framework lock-in) 。智能体生态高度波动——今天的领先者可能明天就被弃用。我们建议围绕稳定接口来设计系统:例如标准化的工具定义(契约)、显式状态 schema、以及基于模式的编排逻辑,而不是把每个组件都紧耦合到某个框架的专有类上。这样的抽象能让你在需求演进时,以最小摩擦迁移或替换框架。
下面给出框架选择的参考:
何时考虑 CrewAI(Consider CrewAI when…)
- 你需要快速原型验证,希望尽快跑起来一个多智能体系统
- 你的工作流天然映射为一个“专家协作团队”(如“researcher”“writer”“editor”)
- 你的流程受益于分层(manager/worker)结构,委派是关键机制
- 通过 context 的隐式状态传递已足够满足需求
何时考虑 LangGraph(Consider LangGraph when…)
- 你需要复杂、非线性的控制流(循环、分支、基于状态的动态路由)
- 你必须在每一步进行显式状态管理与检查,以支撑逻辑或调试
- 你需要基于失败原因进行特定路由的鲁棒错误处理
- 你要求高保真调试与可追踪性(每一步都能看到完整状态)
- 你在构建需要精确状态控制的长运行智能体
- 你需要通过加入“等待输入”的节点,轻松实现 HITL 模式
何时考虑 Google ADK(Consider Google ADK when…)
- 你面向生产级企业环境构建,尤其是在 Google Cloud 生态内
- 你需要更结构化、偏软件工程的方式,把智能体当作可模块化、可测试、可部署的组件
- 与企业标准可观测性(如 OpenTelemetry)与治理系统的集成是首要需求
- 你需要管理智能体的全生命周期:从开发与评估到部署与监控
- 你需要检查进入工具/智能体/模型的 payload,并能检查或对工具/智能体/模型输出采取行动
- 你需要编排复杂多智能体模式(顺序流水线、并行扇出、迭代循环等),并通过专用工作流智能体在 LLM 非确定性推理之上施加确定性的结构
归根结底,框架只是用来落地我们讨论过的模式的工具。理解它们的核心抽象后,你就能选择最匹配问题形态的那一个:
- CrewAI 的 团队(team)
- LangGraph 的 状态机(state machine)
- ADK 的 生产服务(production service)
接下来,我们将把这些框架映射到本书持续讨论的不同“智能体成熟度(agentic maturity)”层级上。
框架作为智能体成熟度的使能器(Frameworks as enablers of agentic maturity)
既然我们已经探讨了构建智能体的实践工具,就可以把这些框架明确映射到我们在第 1 章引入的 **GenAI 成熟度模型(GenAI Maturity Model)**上。这些工具正是“使能器”,帮助组织从基础的、数据增强的生成(Level 2)进阶到真正自治、可协作的智能体系统(Level 4 与 Level 5)。
下表概述了如何推进到各个成熟度等级,并重点说明本章讨论的框架如何在更高的智能体等级加速开发:
表 15.3 —— 按组织成熟度等级选择与推进框架(Approaching frameworks according to the organization's maturity level)
| Maturity level | Description | Framework approach/enabling tools |
|---|---|---|
| Level 1 – Prompting | 简单、单轮提示(single-turn prompting) | 工具:直接调用 LLM API(例如 Gemini、OpenAI)。通常不需要框架。 |
| Level 2 – RAG | 上下文增强生成(RAG) | 工具:LangChain(用于 RAG 流水线),或自研代码:调用向量数据库并把上下文插入 prompt。 |
| Level 3 – Tuning | 与智能体框架无关(N/A to the agentic framework) | —— |
| Level 4 – Grounding and evaluation | CrewAI 与 LangGraph 体现两种不同哲学:CrewAI 偏高层、基于角色的编排;LangGraph 提供低层、由状态驱动的框架。它们在评测与 grounded(锚定)上的做法也反映了这一分野。 | CrewAI:集成化、偏企业“开箱即用” \nGrounding & guardrails: \n- 幻觉护栏(Enterprise):原生功能,为输出分配忠实度分数(0–10),低于阈值触发自我纠正。 \n- 内置 RAG 与知识:原生 knowledge 组件支持 PDF/CSV,使智能体默认可锚定本地数据。 \n- 原生实用工具:如 TimeAwarenessTool,将智能体锚定到现实事实(例如当前日期),减少时间相关幻觉。 \nEvaluation: \n- CrewAI Test CLI:将一个 crew 运行 次,生成性能评分表。 \n- Patronus AI 与训练环:一等公民式支持自动化评测,以及基于人类反馈微调的 crew.train() 方法。 \n\nLangGraph:架构化、开发者主导 \nLangGraph 把 grounding 与评测视为可定制的结构组件,提供对“推理路径”的细粒度控制。 \nGrounding & guardrails: \n- 自我纠正回路:用条件边检测不佳输出并将 state 路由回“Refinement”节点,形成程序化的 grounding loop。 \n- 状态检查点:原生持久化允许系统锚定到“可版本控制”的对话历史,可回滚到已知良好状态。 \n- HITL(Human-in-the-Loop):显式 interrupts,在高风险工具调用前暂停执行,等待人工核验。 \nEvaluation: \n- LangSmith 集成:深度 trace 级评测,可用 “LLM-as-a-judge” 模式对每个节点迁移测量延迟、成本与准确率。 \n- 可单测的节点:节点是隔离的 Python 函数,开发者可在全系统集成前对特定逻辑门进行确定性单元测试。 |
| Level 5 – Single-agent systems | 一个自治智能体,具备 planner、tools 与 memory,执行多步骤任务 | **LangGraph:用包含一个或多个 agent 节点的图,基于显式 state 调用多个工具,并可能循环(反思)。 \nADK:主要用例:定义一个包含工具的单 Agent 类,在 agent runtime 中运行。 \nCrewAI:**也可用“单人 crew”,但不常见。 |
| Level 6 – Multi-agent systems | 多个智能体协作、协商并委派任务,以解决复杂问题 | **LangGraph:非常适合复杂交互:每个智能体/函数作为一个节点,边定义通信、交接与控制流;显式 state 支撑共享理解。 \nCrewAI:核心设计哲学:定义具有不同角色的 crew,以及用于协作的流程(sequential/hierarchical)。 \nADK:**多个独立部署的 ADK 智能体服务,通过消息或 A2A 协议通信。 |
如表所示,框架是把“已具备智能体能力的模型(Level 3)”连接到可用的智能体系统(Level 4 与 Level 5)的桥梁:它们提供了构建我们所设计的复杂应用所需的关键“怎么做”。
接下来我们在下一节结束本章。
总结(Summary)
在本章中,我们探讨了三种主流智能体框架——Google 的 ADK、CrewAI 与 LangGraph,并看到它们如何分别提供不同但同样强大的抽象,用 Google Gemini 作为 LLM 来构建复杂的智能体系统。
我们基于修订后的 Notebook 对贷款处理智能体做了实践实现:CrewAI 展示了通过分层流程对协作团队建模的优势;LangGraph 则通过状态机方式展示了细粒度控制、显式状态管理以及鲁棒的错误处理。我们也将 Google 的 ADK 定位为企业级工具包,聚焦于构建、测试与部署鲁棒、可管理的智能体服务的全生命周期。
我们把这些框架与 GenAI 成熟度模型关联起来,指出它们是迈向 **Level 5(单智能体系统)**与 **Level 6(多智能体系统)**的关键使能器。最后,我们强调:这些高级系统要求在可观测性与治理上达到成熟水平,需要借助 LangSmith 与 OpenTelemetry 等工具来确保可追踪性,而可追踪性是负责任 AI 的核心组成部分。
本章的关键结论如下:
- 框架是加速器:你不必从零构建智能体 planner、状态管理器与工具分发器。CrewAI、LangGraph 与 ADK 等框架提供了必要抽象,使你能够高效构建 Level 4 与 Level 5 系统。
- 选择合适的抽象:框架选择应匹配问题形态。协作、角色分工且强调委派的任务,用 CrewAI 的“团队隐喻”;需要复杂循环过程、显式状态与对流程/错误细粒度控制的任务,用 LangGraph 的“状态机”;面向企业级、可测试、可治理、可运维的服务,用 ADK 的“生产智能体”模型。
- 控制流与错误处理是关键区分点:要获得鲁棒性,必须超越简单的线性序列。LangGraph 的显式图结构提供了管理复杂分支、循环与错误处理的强大方式(如我们校验示例所示),对可靠应用至关重要。CrewAI 的分层流程则提供更简洁的委派模型。
- 可观测性不可妥协:智能体系统很复杂。追溯“智能体为何做出某个决策”的能力(例如借助 LangSmith,尤其在 LangGraph 的显式状态下)不仅是调试功能,更是治理、安全与负责任 AI 的基础要求。
至此,我们已经从 GenAI 的基础概念出发,走过了构建复杂自治智能体系统所需的架构模式与实践框架。在最后一章,我们将把这些概念汇总起来,给出清晰的行动计划,帮助你落地这些模式、沿成熟度模型推进,并引领组织完成转型。