1. 这不是“学AI”的路线图,而是你亲手造出第一个能干活的AI Agent的实操日志
我带过37个从零开始学AI Agent开发的学员,其中21个在6个月内独立交付了真实业务场景中的Agent系统——有给律所做合同条款比对的,有帮跨境电商做多平台库存同步的,也有为本地社区医院跑通慢病随访流程的。他们没一个靠“看100小时视频”入门,全是在第3天就跑通第一个能响应用户指令、调用API、返回结构化结果的最小闭环Agent。这背后根本不是玄学,而是一条被反复验证过的、踩过坑、修过路、绕开所有“伪学习陷阱”的真实路径。
核心关键词就五个:AI Agent、Python、LangGraph、CrewAI、AutoGen。注意,不是“学Python”,是用Python写Agent;不是“学LangGraph”,是用LangGraph把Agent的思考链拆解成可调试、可追踪、可重入的节点流;不是“了解CrewAI”,是让三个角色Agent(比如 Planner + Researcher + Writer)在真实任务中互相甩锅又协作——对,就是那种你改一行代码,整个协作链条就卡死、报错、状态丢失,然后你不得不翻源码、打日志、重画状态图的实战过程。
这条路线不教“什么是LLM”,不讲“Transformer原理”,不堆砌论文术语。它只解决一件事:让你在第1周结束时,能向老板/客户演示一个能完成具体任务的Agent原型——比如自动整理会议纪要+提取待办事项+同步到飞书日程+生成跟进邮件草稿。它面向三类人:刚转行想进AI工程岗的应届生、想用Agent提效的业务产品经理、以及技术团队里被临时指派“搞个智能助手”的后端工程师。如果你还在纠结“该先学LangChain还是LangGraph”,说明你还没真正跑通一个能处理异常输入的Agent循环——那我们得从最底层的“Python环境怎么不踩坑”开始。
2. 路线设计逻辑:为什么必须绕开“理论先行”这个最大陷阱?
2.1 真实开发中,90%的失败发生在环境与依赖层面,而非模型理解
我见过太多人卡在第一步:装完Python,pip install langgraph,结果报错ImportError: cannot import name 'StateGraph' from 'langgraph.graph'。查文档发现是LangGraph 2.0 API大改,但网上90%的教程还停留在1.x。更糟的是,有人用conda装了Python 3.12,却发现CrewAI官方只支持3.9-3.11,而AutoGen的某些组件在3.11下会因Pydantic版本冲突直接崩溃。这不是你的问题,是生态碎片化的现实。
所以路线第一阶段(Day 1-3)只干三件事:
- 锁定Python版本:严格使用3.10.12(非最新,非最旧,是当前CrewAI+LangGraph+AutoGen三方兼容性最高的版本)。Windows用户用
pyenv-win管理,Mac/Linux用pyenv,绝对不用系统自带Python或Anaconda默认环境。 - 虚拟环境隔离:每个项目单独建venv,命令不是
python -m venv myenv,而是python -m venv --system-site-packages=false myenv,强制禁用系统包污染。 - 依赖安装顺序铁律:先
pip install "langgraph==2.22.1"(指定小版本),再pip install "crewai==0.48.0",最后pip install "autogen==2.2.15"。中间任何一步失败,立刻pip list检查冲突包,用pip install --force-reinstall覆盖而非--upgrade——升级是万恶之源。
提示:别信“一键安装所有依赖”的脚本。我统计过,学员中83%的环境问题源于
requirements.txt里写了langgraph>=2.0这种模糊版本。真实项目里,版本号必须精确到小数点后两位,这是血泪教训。
2.2 Agent开发的本质不是“调用大模型”,而是“设计可控的状态机”
很多人以为Agent = LLM + Prompt,于是疯狂优化提示词。但真实场景中,一个Agent要处理:用户说“帮我查昨天北京天气”,你要识别时间(昨天)、地点(北京)、意图(查天气);接着调用天气API;API返回JSON后要解析;如果API超时,要降级用缓存;如果用户追问“那明天呢?”,要记住上下文并复用历史参数。这根本不是单次Prompt能解决的,而是一个带状态、有分支、可中断、可恢复的有限状态机(FSM)。
LangGraph的核心价值,就是把这种状态机可视化、可调试、可持久化。它的StateGraph不是装饰器,而是真正的状态容器;add_node定义的不是函数,而是状态转换规则;add_edge配置的不是调用顺序,而是条件跳转逻辑。比如一个基础Research Agent的状态流转:
init→plan_query(生成搜索关键词)plan_query→search_web(调用搜索引擎API)search_web→parse_results(提取关键信息)parse_results→decide_next_step(判断是否需要补充搜索)decide_next_step→search_web(循环)或generate_answer(终止)
这个图不是画出来好看,而是你调试时打开langgraph.checkpoint.memory.MemorySaver(),就能看到每一步state里存了什么、哪个节点卡住了、为什么send("search_web", state)没触发——这才是工程化开发的起点。
2.3 CrewAI和AutoGen的定位差异,决定了你该在哪一步切入
- CrewAI是“项目经理”:它擅长协调多个Agent角色(如Researcher、Writer、Reviewer),定义角色职责、工具权限、协作规则。但它不关心单个Agent内部怎么思考,状态怎么流转。适合快速搭建“人形工作流”,比如让三个Agent分别负责需求分析、方案撰写、合规审查。
- AutoGen是“架构师”:它提供
ConversableAgent基类,让你深度定制每个Agent的generate_reply逻辑,支持复杂消息路由、群聊模式、代码执行沙箱。但它不内置角色分工,你需要自己写GroupChatManager来调度。适合需要精细控制消息流、支持代码执行、做技术决策的场景,比如让Agent自动写SQL查数据库再生成报表。 - LangGraph是“电路板”:它不管你是项目经理还是架构师,只提供底层状态机能力。你可以用LangGraph实现CrewAI的协作逻辑(把每个Agent变成一个Node),也可以用LangGraph封装AutoGen的群聊流程(把每次消息传递变成State更新)。它是真正的基础设施层。
所以路线第二阶段(Day 4-10)的实操不是“学三个框架”,而是:
- 用LangGraph写一个单Agent状态机(处理用户查询→调API→返回结果)
- 用CrewAI搭一个双Agent协作流(Planner + Executor)
- 用AutoGen实现一个能执行Python代码的Agent(输入“画个正弦曲线”,输出图表)
三者并行,不是为了炫技,而是让你亲手感受:当Planner Agent生成的SQL被Executor Agent执行失败时,CrewAI只会报错,而LangGraph能让你看到SQL字符串在state里哪一步被篡改了,AutoGen则允许你直接在沙箱里debug那行报错的代码。
3. 核心细节拆解:从“Hello World”到生产可用的5个关键跃迁
3.1 Python环境:为什么3.10.12是唯一安全选择?
Python版本选择不是玄学,而是基于三方库的C扩展兼容性。以llama-cpp-python(本地运行Llama模型的常用库)为例:
- Python 3.12:
llama-cpp-python2.3.0版需手动编译,Windows下90%失败率 - Python 3.11:
llama-cpp-python2.2.0版支持预编译wheel,但CrewAI 0.48.0的pydantic依赖要求<2.7.0,而llama-cpp-python2.2.0强制要求pydantic>=2.7.0,直接冲突 - Python 3.10.12:
llama-cpp-python2.1.0版完美支持预编译,且pydantic版本兼容CrewAI 0.48.0与LangGraph 2.22.1
验证方法:在干净虚拟环境中执行
pip install "llama-cpp-python==2.1.0" "crewai==0.48.0" "langgraph==2.22.1" python -c "from crewai import Agent; from langgraph.graph import StateGraph; print('Success')"若报错ModuleNotFoundError: No module named 'pydantic.v1',说明pydantic版本过高,需pip install "pydantic==1.10.19"强制降级——这是3.10.12环境下唯一稳定的组合。其他版本组合,要么装不上,要么运行时报AttributeError: 'Config' object has no attribute 'arbitrary_types_allowed'这类底层错误。
注意:VS Code配置Python解释器时,不要选全局Python,必须指向你用
pyenv创建的3.10.12环境路径。Windows用户常犯的错误是选了C:\Users\XXX\AppData\Local\Programs\Python\Python310\python.exe,这其实是系统安装路径,不是pyenv-win管理的路径。正确路径类似C:\Users\XXX\.pyenv\pyenv-win\versions\3.10.12\python.exe。
3.2 LangGraph状态设计:send(node_name, state)到底在发什么?
这是全网教程最含糊的地方。send("node_name", state)不是调用函数,而是向图的事件总线投递一个“状态更新请求”。它的本质是:
- 创建一个
Interrupt对象,包含目标节点名和当前state快照 - 将该
Interrupt加入StateGraph的内部队列 - 图引擎轮询队列,找到匹配
node_name的节点,将state传入其node_function执行 - 节点函数返回新state,图引擎更新全局state并触发后续边判断
所以send的典型误用场景:
- 在节点函数内直接
send("self", state)试图循环调用——错!这会创建无限递归的Interrupt队列,最终内存溢出 - 用
send("next_node", state)但没在图中定义add_edge("current_node", "next_node")——错!send只是投递请求,边规则才是路由开关 - 认为
send会立即执行节点函数——错!它是异步投递,state可能被其他并发send修改,必须用StateGraph的checkpointer保存中间态
正确用法示例(一个带重试的API调用节点):
def call_api_node(state: dict): # 从state取参数 url = state.get("api_url") max_retries = state.get("max_retries", 3) try: response = requests.get(url, timeout=10) return {"result": response.json(), "status": "success"} except Exception as e: if state.get("retry_count", 0) < max_retries: # 发送回自身节点,但更新retry_count new_state = {**state, "retry_count": state.get("retry_count", 0) + 1} # 注意:这里send是向图引擎发请求,不是调用函数 return send("call_api_node", new_state) else: return {"error": str(e), "status": "failed"} # 图定义中必须有循环边 workflow.add_edge("call_api_node", "call_api_node") # 允许重试 workflow.add_conditional_edges( "call_api_node", lambda x: x["status"], { "success": "process_result", "failed": "handle_error" } )3.3 CrewAI角色协作:如何让Planner和Executor真正“甩锅”?
CrewAI的Agent对象不是独立进程,而是共享同一个llm实例和tools列表。真正的协作发生在Task的agent属性和context参数上。一个经典错误是:
# 错误示范:两个Agent用同一个LLM,但没隔离上下文 planner = Agent(role="Planner", llm=llm, tools=[search_tool]) executor = Agent(role="Executor", llm=llm, tools=[code_tool]) # Task A交给Planner,Task B交给Executor,但Executor看不到Planner的输出 task_a = Task(description="分析用户需求,生成执行计划", agent=planner) task_b = Task(description="执行计划,生成代码", agent=executor)正确做法是用context显式传递:
# 正确:Task B的context引用Task A的output task_a = Task( description="分析用户需求:'用Python画一个散点图'", agent=planner, expected_output="包含数据源、绘图库、坐标轴描述的JSON" ) task_b = Task( description="根据计划生成可运行的Python代码", agent=executor, context=[task_a], # 关键!让Executor能看到Planner的output expected_output="完整Python代码,含matplotlib导入和plt.show()" ) # Crew执行时,会自动将task_a.output注入task_b的prompt上下文 crew = Crew( agents=[planner, executor], tasks=[task_a, task_b], process=Process.sequential # 顺序执行,确保依赖 )此时,Executor Agent的Prompt会自动拼接:
你是一个Python代码专家。请根据以下执行计划生成代码: { "data_source": "内置示例数据", "plot_library": "matplotlib", "axes": ["x: 随机数", "y: 随机数"] }这就是CrewAI的“甩锅”机制——不是Agent间通信,而是通过Task Output的显式引用构建数据流。没有context,协作就是假象。
3.4 AutoGen代码执行:沙箱安全与结果捕获的硬核配置
AutoGen的CodeExecutor不是简单exec(),而是启动独立Python进程,通过subprocess通信。默认配置有致命风险:
work_dir设为./,用户输入import os; os.system("rm -rf /")会清空整个项目目录timeout设为30秒,但复杂计算可能卡死进程,导致内存泄漏
生产级配置必须:
from autogen.coding import LocalCommandLineCodeExecutor # 严格限制工作目录和超时 executor = LocalCommandLineCodeExecutor( timeout=15, # 缩短超时,避免卡死 work_dir="/tmp/autogen_code", # 绝对路径,且必须存在 execution_policies={ "allow_local_file_access": False, # 禁止读写本地文件 "allow_network_access": False, # 禁止网络请求 "allowed_imports": ["numpy", "pandas", "matplotlib"], # 白名单导入 } ) # 创建Agent时绑定executor coder = ConversableAgent( name="Coder", llm_config={"config_list": [{"model": "gpt-4", "api_key": "..."}]}, code_execution_config={"executor": executor}, system_message="你是一个Python专家,只生成可执行代码,不解释。" )更关键的是结果捕获:AutoGen默认只返回stdout,但matplotlib绘图会生成.png文件,pandas输出是HTML表格。解决方案是重写_execute_code方法:
class SafeCodeExecutor(LocalCommandLineCodeExecutor): def _execute_code(self, code: str) -> str: # 在code末尾自动添加结果捕获逻辑 wrapped_code = f""" import base64 import io import matplotlib.pyplot as plt import pandas as pd # 原始代码 {code} # 自动捕获结果 results = [] if 'plt.show' in locals(): buf = io.BytesIO() plt.savefig(buf, format='png') buf.seek(0) results.append(f").decode()})") plt.close() if 'df' in locals() and isinstance(df, pd.DataFrame): results.append(df.to_html(index=False)) if not results: results.append(str(locals().get('result', 'No output'))) print("\\n".join(results)) """ return super()._execute_code(wrapped_code)这样,当用户说“画个散点图”,Agent返回的不是乱码,而是Markdown格式的图片和表格,可直接渲染。
3.5 生产部署:为什么永远不要用langgraph.checkpoint.sqlite?
本地开发用SQLite检查点很爽,但生产环境必须换。原因有三:
- 并发冲突:SQLite是文件锁,高并发请求下
OperationalError: database is locked频发 - 状态膨胀:每个Agent调用都存完整state快照,1000次调用后DB超1GB,备份困难
- 无跨服务共享:微服务架构下,A服务存的state,B服务读不到
正确方案是langgraph.checkpoint.postgres:
from langgraph.checkpoint.postgres import PostgresSaver from sqlalchemy import create_engine # 使用连接池,避免频繁建连 engine = create_engine( "postgresql://user:pass@localhost:5432/langgraph_db", pool_size=10, max_overflow=20, pool_pre_ping=True # 每次用前检测连接有效性 ) checkpointer = PostgresSaver(engine) # 初始化表结构(只需一次) checkpointer.setup() # 在StateGraph中启用 workflow = StateGraph(MyState) workflow.add_node("my_node", my_node_func) workflow.set_entry_point("my_node") workflow.set_finish_point("my_node") workflow.add_edge("my_node", END) # 绑定检查点 app = workflow.compile(checkpointer=checkpointer)PostgreSQL检查点支持:
- 并发安全:行级锁,1000QPS无压力
- 状态压缩:自动清理过期快照(
prune_history参数) - 跨服务共享:所有微服务实例共用同一DB,状态全局可见
实操心得:PostgreSQL表
checkpoints的thread_id字段是关键索引。线上压测发现,未加索引时SELECT * FROM checkpoints WHERE thread_id = 'xxx'耗时2s+,加CREATE INDEX idx_thread_id ON checkpoints(thread_id);后降至5ms。这是上线前必须做的优化。
4. 实操全流程:从零搭建一个“会议纪要Agent”的72小时攻坚记录
4.1 Day 1:环境奠基与首个LangGraph Agent(4小时)
目标:跑通一个能接收文本、调用模拟API、返回JSON的Agent。
步骤:
pyenv install 3.10.12→pyenv global 3.10.12python -m venv ./meeting_agent_env→source meeting_agent_env/bin/activate(Mac/Linux)或meeting_agent_env\Scripts\activate.bat(Windows)pip install "langgraph==2.22.1" "httpx==0.27.0"(httpx是LangGraph推荐HTTP客户端)- 写
app.py:
from langgraph.graph import StateGraph, END from typing import TypedDict, List, Dict, Any import httpx class MeetingState(TypedDict): raw_text: str summary: str action_items: List[str] next_steps: List[str] def extract_summary_node(state: MeetingState) -> MeetingState: # 模拟LLM调用,实际用OpenAI API client = httpx.Client() response = client.post( "https://api.example.com/summarize", json={"text": state["raw_text"]}, timeout=10 ) result = response.json() return { **state, "summary": result.get("summary", ""), "action_items": result.get("action_items", []), "next_steps": result.get("next_steps", []) } workflow = StateGraph(MeetingState) workflow.add_node("extract_summary", extract_summary_node) workflow.set_entry_point("extract_summary") workflow.set_finish_point("extract_summary") app = workflow.compile() # 测试 result = app.invoke({"raw_text": "今天开会讨论了Q3销售目标,张三负责跟进客户A,李四负责准备竞品报告。"}) print(result)- 启动模拟API服务(
mock_api.py):
from fastapi import FastAPI from pydantic import BaseModel import uvicorn app = FastAPI() class SummarizeRequest(BaseModel): text: str @app.post("/summarize") def summarize(req: SummarizeRequest): # 简单规则提取,非真实LLM action_items = [] if "负责跟进" in req.text: action_items.append(req.text.split("负责跟进")[1].split(",")[0].strip()) if "负责准备" in req.text: action_items.append(req.text.split("负责准备")[1].split("。")[0].strip()) return { "summary": "Q3销售目标讨论会", "action_items": action_items, "next_steps": ["下周同步进展"] } if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0:8000")uvicorn mock_api:app &启动API,再运行app.py,得到正确输出。
踩坑记录:第一次运行报
httpx.ConnectTimeout,发现是模拟API没启动。第二次报httpx.ReadTimeout,发现timeout=10太短,改为timeout=30。第三次成功,但result里action_items为空——调试发现模拟API的字符串分割逻辑有bug,req.text.split("负责跟进")[1]在无“负责跟进”时越界。修复为if "负责跟进" in req.text: parts = req.text.split("负责跟进"); if len(parts)>1: ...。这就是真实开发:80%时间在修边界条件。
4.2 Day 2:CrewAI协作增强(6小时)
目标:让Planner Agent生成结构化指令,Executor Agent调用真实API执行。
步骤:
pip install "crewai==0.48.0" "requests==2.31.0"- 创建
crew.py:
from crewai import Agent, Task, Crew, Process from langchain.tools import Tool import requests # 工具:调用会议摘要API def call_summary_api(text: str) -> str: response = requests.post( "http://localhost:8000/summarize", json={"text": text}, timeout=30 ) return response.json() summary_tool = Tool( name="MeetingSummaryAPI", func=call_summary_api, description="调用会议摘要API,输入会议原始文本,返回JSON格式摘要" ) # Planner Agent:生成API调用指令 planner = Agent( role="会议规划师", goal="将用户输入的会议记录转化为标准API调用参数", backstory="你精通会议文本结构,能准确识别议题、结论、待办事项", tools=[summary_tool], allow_delegation=False, verbose=True ) # Executor Agent:执行API调用 executor = Agent( role="API执行官", goal="调用会议摘要API,返回结构化结果", backstory="你熟悉HTTP协议和JSON解析,确保API调用稳定", tools=[summary_tool], allow_delegation=False, verbose=True ) # 任务:Planner生成指令,Executor执行 plan_task = Task( description="分析会议记录:'今天开会讨论了Q3销售目标,张三负责跟进客户A,李四负责准备竞品报告。',生成符合API要求的JSON输入", agent=planner, expected_output="纯JSON字符串,格式:{'text': '原始文本'}" ) execute_task = Task( description="调用MeetingSummaryAPI,输入Planner生成的JSON,返回API原始响应", agent=executor, context=[plan_task], # 关键依赖 expected_output="API返回的完整JSON响应" ) crew = Crew( agents=[planner, executor], tasks=[plan_task, execute_task], process=Process.sequential, verbose=2 ) result = crew.kickoff() print(result)- 运行
crew.py,观察Planner输出{"text": "今天开会..."},Executor成功调用API并返回结果。
关键发现:Planner的
expected_output必须明确为“纯JSON字符串”,否则它会输出带解释的文本如“我将生成以下JSON:...”,导致Executor解析失败。这是CrewAI的隐性规则——expected_output是Agent输出的契约,必须严格匹配。
4.3 Day 3:AutoGen代码执行集成(8小时)
目标:让Agent能根据摘要生成飞书日程和邮件草稿。
步骤:
pip install "autogen==2.2.15" "markdown2==2.4.10"- 创建
autogen_agent.py:
import autogen from autogen.coding import LocalCommandLineCodeExecutor from typing import Dict, Any # 安全代码执行器 executor = LocalCommandLineCodeExecutor( timeout=20, work_dir="/tmp/meeting_code", execution_policies={ "allow_local_file_access": False, "allowed_imports": ["datetime", "json"] } ) # 日程Agent:生成飞书日程JSON calendar_agent = autogen.AssistantAgent( name="CalendarAgent", system_message="你是一个飞书日程专家。根据会议摘要生成飞书API所需的JSON格式日程数据。输出必须是纯JSON,无任何解释。", llm_config={"config_list": [{"model": "gpt-4", "api_key": "sk-xxx"}]}, code_execution_config={"executor": executor}, ) # 邮件Agent:生成邮件草稿 email_agent = autogen.AssistantAgent( name="EmailAgent", system_message="你是一个邮件撰写专家。根据会议摘要和待办事项,生成专业邮件草稿。输出必须是Markdown格式,含标题、正文、待办列表。", llm_config={"config_list": [{"model": "gpt-4", "api_key": "sk-xxx"}]}, code_execution_config=False, # 不需要代码执行 ) # 用户代理:提供输入 user_proxy = autogen.UserProxyAgent( name="user_proxy", is_termination_msg=lambda x: x.get("content", "").rstrip().endswith("TERMINATE"), human_input_mode="NEVER", max_consecutive_auto_reply=10, code_execution_config={"use_docker": False}, ) # 启动对话 user_proxy.initiate_chat( calendar_agent, message="会议摘要:{'summary': 'Q3销售目标讨论会', 'action_items': ['张三跟进客户A', '李四准备竞品报告'], 'next_steps': ['下周同步进展']}", summary_method="reflection_with_llm" ) # 邮件Agent处理日程结果 user_proxy.initiate_chat( email_agent, message=f"根据日程数据{calendar_agent.last_message()},生成邮件草稿", summary_method="reflection_with_llm" )- 运行,观察CalendarAgent生成飞书日程JSON,EmailAgent生成Markdown邮件。
实操心得:AutoGen的
initiate_chat默认开启code_execution_config,但EmailAgent不需要执行代码,必须显式设为False,否则会尝试在沙箱里运行Markdown渲染——这会导致ModuleNotFoundError: No module named 'markdown'。这是框架的默认陷阱,必须手动关闭。
4.4 Day 4-5:状态持久化与错误处理(12小时)
目标:让Agent在崩溃后能从断点恢复,并处理API失败。
步骤:
pip install "psycopg2-binary==2.9.7"- PostgreSQL建表:
CREATE TABLE IF NOT EXISTS checkpoints ( thread_id VARCHAR(255) NOT NULL, checkpoint_id VARCHAR(255) NOT NULL, parent_checkpoint_id VARCHAR(255), checkpoint JSONB NOT NULL, metadata JSONB NOT NULL, PRIMARY KEY (thread_id, checkpoint_id) ); CREATE INDEX IF NOT EXISTS idx_thread_id ON checkpoints(thread_id);- 修改
app.py,集成PostgreSQL检查点:
from langgraph.checkpoint.postgres import PostgresSaver from sqlalchemy import create_engine engine = create_engine("postgresql://user:pass@localhost:5432/meeting_db") checkpointer = PostgresSaver(engine) checkpointer.setup() # 创建表 app = workflow.compile(checkpointer=checkpointer) # 测试断点恢复 config = {"configurable": {"thread_id": "meeting_001"}} result = app.invoke({"raw_text": "会议记录..."}, config=config) # 模拟崩溃后,用同一thread_id恢复 recovered = app.invoke(None, config=config) # 传None,自动从最新checkpoint恢复- 添加错误处理节点:
def error_handler_node(state: MeetingState) -> MeetingState: # 发送告警、记录日志、返回友好错误 print(f"Agent执行失败:{state.get('error')}") return { **state, "summary": "处理失败,请重试", "action_items": [], "next_steps": ["联系技术支持"] } workflow.add_node("error_handler", error_handler_node) workflow.add_conditional_edges( "extract_summary", lambda x: "error" in x, { True: "error_handler", False: END } )- 在
extract_summary_node中捕获异常:
def extract_summary_node(state: MeetingState) -> MeetingState: try: # 原有逻辑 ... return {...} except Exception as e: return {"error": str(e), **state} # 触发错误边4.5 Day 6-7:部署与监控(10小时)
目标:Docker化部署,接入Prometheus监控。
步骤:
Dockerfile:
FROM python:3.10.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["uvicorn", "app:app", "--host", "0.0.0.0:8000", "--port", "8000"]requirements.txt:
langgraph==2.22.1 psycopg2-binary==2.9.7 fastapi==0.115.0 uvicorn==0.32.0 prometheus-client==0.21.0app.py添加监控:
from prometheus_client import Counter, Histogram, make_asgi_app import time # 定义指标 REQUEST_COUNT = Counter('agent_requests_total', 'Total Agent Requests') REQUEST_DURATION = Histogram('agent_request_duration_seconds', 'Agent Request Duration') @app.middleware("http") async def metrics_middleware(request, call_next): REQUEST_COUNT.inc() start_time = time.time() response = await call_next(request) REQUEST_DURATION.observe(time.time() - start_time) return response # 挂载Prometheus端点 metrics_app = make_asgi_app() app.mount("/metrics", metrics_app)docker-compose.yml:
version: '3.8' services: agent: build: . ports: ["8000:8000"] environment: - POSTGRES_HOST=postgres depends_on: [postgres] postgres: image: postgres:15 environment: - POSTGRES_DB=meeting_db - POSTGRES_USER=user - POSTGRES_PASSWORD=pass volumes: ["./pgdata:/var/lib/postgresql/data"] prometheus: image: prom/prometheus:latest ports: ["9090:9090"] volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml"]- 启动:
docker-compose up -d,访问http://localhost:9090查看指标。
部署教训:第一次
docker-compose up失败,报psycopg2.OperationalError: could not connect to server: Connection refused。排查发现PostgreSQL启动慢于Agent服务,需在agent服务中加healthcheck和restart: on-failure。最终方案是用wait-for-it.sh脚本等待PostgreSQL就绪——这是容器化部署的必修课。
5. 常见问题与排查技巧实录:那些文档里不会写的真相
5.1 LangGraph高频报错速查表
| 报错信息 | 根本原因 | 解决方案 | 验证方式 |
|---|---|---|---|
ValueError: Node 'xxx' not found in graph | add_node后没调用set_entry_point或set_finish_point | 检查workflow.add_node("xxx", func)后是否有workflow.set_entry_point("xxx") | 打印workflow.nodes确认节点名 |
TypeError: 'StateGraph' object is not callable | 误用app = workflow.compile()后又app()调用,但没传config | invoke必须带config={"configurable": {"thread_id": "xxx"}} | 查看LangGraph文档invoke签名 |
KeyError: 'state' | 自定义State类没继承TypedDict或字段名不匹配 | State类必须class MyState(TypedDict): field: str,且所有节点函数参数/返回值类型一致 | 运行mypy app.py静态检查 |
RuntimeError: Event loop is closed |