news 2026/11/3 20:41:57

从手动实践到LangGraph框架:智能体开发实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从手动实践到LangGraph框架:智能体开发实战与避坑指南

1. 从手动实践到框架开发:为什么我最终选择了智能体框架

1.1 手动实践阶段踩过的那些坑

刚开始接触智能体开发的时候,我和很多人一样,觉得这东西没什么复杂的。无非就是调个大模型接口,拼一段提示词,把工具函数塞进去,然后循环调用嘛。于是我用最原始的方式手搓了一个“智能助手”:一个Python脚本,里面写死了几个工具函数,用while循环判断模型返回的内容里有没有工具调用标记,有就执行工具再把结果拼回对话历史,没有就直接输出。

这个方案跑通第一个demo的时候确实挺爽的,几十行代码就能让模型查天气、算数学题、搜索本地文档。但问题很快就来了。第一个坑是对话状态管理。手动维护一个messages列表,每次都要小心翼翼地处理role字段,工具调用的结果要按特定格式塞回去,稍有不慎模型就“失忆”了,或者把工具返回的JSON当成了用户说的话。第二个坑是多轮工具调用的嵌套。当模型需要先查天气、再根据天气推荐穿衣、再根据穿衣推荐购物链接时,手动循环的逻辑就开始变得像意大利面条,if-else套了好几层,调试的时候只能靠print大法。

第三个坑最致命:错误处理。模型有时候会返回格式不对的JSON,有时候会调用不存在的工具,有时候会在工具执行超时后继续往下编。手动写的代码里,这些异常全靠try-except兜底,但兜着兜着就发现,整个流程的状态已经乱了,根本不知道下一步该从哪里恢复。那段时间我经常对着终端里滚动的日志发呆,心想这玩意儿要是上线了,用户随便一句话就能让它崩溃。

1.2 框架开发带来的思维转变

痛定思痛之后,我开始认真研究主流的智能体框架。从AutoGen到LangGraph,从LangChain到一些新兴的轻量级方案,我花了两周时间把每个框架的官方示例都跑了一遍。跑完之后最大的感受不是“哪个框架更强”,而是思维方式的转变。

手动实践的时候,我脑子里想的是“怎么让模型按我的流程走”。框架开发的时候,我脑子里想的是“怎么把任务拆成节点,让模型在节点之间流转”。这个转变听起来很虚,但实际写代码的时候差别巨大。举个例子,手动实现一个“先搜索再总结”的流程,我需要写一个函数,里面先调搜索工具,拿到结果后再拼一段提示词调模型总结。而在LangGraph里,我只需要定义两个节点:一个search_node,一个summarize_node,然后用一条边把它们连起来。搜索节点的输出自动成为总结节点的输入,状态在图的流转中自动维护。

这种声明式的编程方式,让我从“怎么管理状态、怎么处理异常、怎么控制循环”这些琐事里解放出来,专注于“任务本身应该怎么拆解”。更重要的是,框架帮我处理了那些手动实践时最容易出错的边界情况:工具调用格式的校验、对话历史的截断、超时重试、并发控制。这些东西自己写不是写不出来,但写出来之后你会发现,你花在维护这套基础设施上的时间,远远超过了写业务逻辑的时间。

1.3 主流框架的选型对比与思考

在选框架的时候,我对比了几个主流方案。AutoGen的特点是多智能体对话,它把多个智能体之间的交互抽象成了“群聊”,适合那种需要多个角色协作的场景,比如一个负责写代码、一个负责审查、一个负责测试。但它的抽象层次比较高,有时候我想精细控制某个节点的行为,反而觉得束手束脚。

LangGraph则是图结构的思路,每个节点是一个函数,边定义了节点之间的流转条件。它的控制粒度更细,适合需要精确控制流程的场景,比如“如果搜索结果为空就走兜底逻辑,否则走总结逻辑”。而且LangGraph和LangChain生态无缝集成,工具调用、记忆管理、回调系统都是现成的。

LangChain本身更像是一个工具库,它提供了大量的组件:各种模型的封装、各种工具的封装、各种记忆机制的封装。但如果你只用LangChain的Chain,不做图结构的编排,复杂流程还是会写得很乱。所以我现在的做法是:用LangChain的组件做基础能力,用LangGraph做流程编排,两者配合使用。

至于一些新兴的轻量级框架,比如Agno,它的优势是简单直接,适合快速验证想法。但如果你要做生产级的应用,需要考虑可观测性、可扩展性、错误恢复这些,还是得用更成熟的方案。

框架核心抽象适合场景学习曲线生产就绪度
AutoGen多智能体群聊多角色协作、代码生成与审查中等较高
LangGraph状态图复杂流程编排、精确控制中等偏上高
LangChain组件库快速搭建原型、工具集成较低高
Agno轻量级Agent快速验证、简单任务低中等

选型的时候我的建议是:先想清楚你的任务复杂度。如果只是单轮的工具调用,手动写或者用LangChain的AgentExecutor就够了。如果是多轮、多分支、需要状态持久化的流程,直接上LangGraph。如果是多个智能体需要互相协作、辩论、投票,AutoGen会更顺手。

2. 智能体框架的核心概念拆解

2.1 状态管理:框架最核心的价值

状态管理是智能体框架最核心的价值,没有之一。手动实践的时候,状态就是一个messages列表,你往里面append,模型从里面读。但实际应用中,状态远不止对话历史这么简单。它还包括:当前执行到哪个步骤了、上一步的输出是什么、有没有中间结果需要缓存、用户的偏好设置是什么、这次会话的上下文窗口还剩多少。

LangGraph用TypedDict来定义状态,每个节点接收当前状态,返回状态的更新。这个设计的好处是,状态的变更路径是显式的,你可以清楚地看到每个节点对状态做了什么修改。而且LangGraph支持状态持久化,你可以把状态存到数据库里,下次会话直接从上次的状态恢复。这个功能对于长对话场景特别有用,用户今天聊到一半关掉了,明天打开还能接着聊。

手动实现状态持久化的话,你需要自己序列化messages列表,处理各种边界情况,比如工具调用的中间状态怎么存、模型返回的流式内容怎么存。这些细节框架都帮你处理好了。

2.2 工具调用:从函数注册到自动编排

工具调用是智能体的“手脚”。手动实践的时候,工具就是一个Python函数,你需要自己写一段描述告诉模型这个工具是干什么的、参数是什么格式。模型返回工具调用请求后,你需要自己解析JSON、执行函数、把结果格式化后塞回对话历史。

框架把这个过程标准化了。在LangChain里,你只需要用@tool装饰器标记一个函数,框架会自动提取函数的docstring作为工具描述,自动生成参数schema。模型返回工具调用请求后,框架会自动执行对应的函数,并把结果按标准格式返回给模型。你不需要关心JSON怎么解析、结果怎么格式化,只需要关心工具本身的逻辑。

更高级的是工具编排。当模型需要连续调用多个工具时,框架会自动处理工具之间的依赖关系。比如模型先调用search工具拿到文档ID,再调用read_document工具读取文档内容。手动实现的话,你需要写一个循环来不断检查模型是否还有工具调用请求。框架里这个循环是内置的,你只需要定义工具,剩下的交给框架。

2.3 记忆机制:短期记忆与长期记忆的配合

记忆机制是智能体框架里比较容易被忽视,但实际非常重要的部分。短期记忆就是对话历史,这个手动实践和框架开发差别不大,都是维护一个消息列表。但长期记忆就不一样了,它涉及到信息的提取、存储和检索。

框架通常提供几种记忆方案:一种是滑动窗口,只保留最近N轮对话,简单粗暴但有效;一种是摘要记忆,把历史对话压缩成摘要,节省token但会丢失细节;还有一种是向量记忆,把历史对话存到向量数据库里,需要的时候检索相关片段。

LangGraph里,记忆是通过checkpointer来实现的。你可以把状态存到内存、SQLite、Postgres等后端。每次节点执行前,框架会自动从checkpointer加载状态;节点执行后,自动保存状态。这个机制让“断点续聊”变得非常简单。

手动实现长期记忆的话,你需要自己设计存储结构、自己写检索逻辑、自己处理记忆的更新和淘汰。框架把这些都抽象成了配置项,你只需要选择用哪种记忆方案,剩下的框架来处理。

2.4 流程编排:从线性执行到图结构

流程编排是区分“玩具”和“产品”的关键。手动实践的时候,流程通常是线性的:用户输入 -> 模型思考 -> 工具调用 -> 模型总结 -> 输出。但实际应用中,流程往往是有分支的:如果搜索结果为空,走兜底逻辑;如果用户意图是查询,走查询流程;如果用户意图是修改,走修改流程。

LangGraph用图来表达流程。节点是执行单元,边是流转条件。你可以定义条件边,根据当前状态决定下一步走哪个节点。你还可以定义循环,让流程在满足条件时回到之前的节点。这种图结构的表达能力比线性流程强太多了。

举个例子,一个客服智能体的流程可能是:先判断用户意图,如果是咨询产品,走产品查询节点;如果是投诉,走投诉处理节点;如果是闲聊,走闲聊节点。每个节点执行完后,再判断是否需要转人工。这个流程用图来表达非常清晰,每个节点的职责单一,节点之间的流转条件明确。

3. 基于LangGraph的智能体框架实战

3.1 环境准备与依赖安装

我用的环境是Python 3.11,这个版本对异步的支持比较完善,而且和主流框架的兼容性最好。依赖管理我用的是uv,比pip快很多,而且能自动处理虚拟环境。如果你习惯用pip也没问题,只是安装速度会慢一些。

# 创建虚拟环境 uv venv agent-env source agent-env/bin/activate # 安装核心依赖 uv pip install langgraph langchain langchain-openai langchain-community uv pip install python-dotenv tavily-python

这里解释一下每个依赖的作用。langgraph是流程编排的核心,langchain提供了工具和模型的封装,langchain-openai是OpenAI模型的适配器,langchain-community里有很多现成的工具集成。python-dotenv用来管理环境变量,tavily-python是一个搜索工具的客户端,后面会用到。

注意:如果你用的是其他模型提供商,比如Anthropic或者国内的模型,需要安装对应的适配器包。LangChain的模型适配器是统一的接口,切换模型只需要改一行代码。

环境变量文件.env里配置API密钥:

OPENAI_API_KEY=your_key_here TAVILY_API_KEY=your_key_here

3.2 定义状态结构:智能体的“记忆骨架”

状态结构决定了智能体能记住什么、能传递什么。我定义了一个AgentState,包含四个字段:messages是对话历史,next_step是下一步要执行的节点,search_results是搜索结果的缓存,user_intent是识别出的用户意图。

from typing import Annotated, TypedDict, Literal from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] next_step: str search_results: list[str] user_intent: Literal["search", "chat", "unknown"]

add_messages是一个reducer函数,它定义了messages字段的更新方式。每次节点返回新的消息时,add_messages会把新消息追加到现有列表里,而不是覆盖。这个设计很巧妙,节点不需要关心历史消息,只需要返回本次新增的消息即可。

user_intent用了Literal类型,限制只能是search、chat、unknown三个值之一。这个约束在开发阶段很有用,如果节点返回了其他值,类型检查会直接报错,避免运行时才发现问题。

3.3 工具定义与注册:让智能体有“手脚”

我定义了两个工具:一个是搜索工具,用Tavily的API;一个是计算器工具,用来处理数学运算。工具的定义用@tool装饰器,函数的docstring会自动成为工具描述。

from langchain_core.tools import tool from tavily import TavilyClient import os tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) @tool def web_search(query: str) -> str: """搜索互联网获取最新信息。当用户询问实时新闻、当前事件或需要最新数据时使用此工具。 Args: query: 搜索关键词,尽量具体明确 """ try: results = tavily_client.search(query, max_results=3) formatted = [] for r in results["results"]: formatted.append(f"标题:{r['title']}\n内容:{r['content'][:200]}") return "\n---\n".join(formatted) except Exception as e: return f"搜索失败:{str(e)}" @tool def calculator(expression: str) -> str: """计算数学表达式。支持加减乘除、幂运算、三角函数等。 Args: expression: 数学表达式,如 "2 + 3 * 4" 或 "sin(pi/2)" """ try: import math allowed = {k: v for k, v in math.__dict__.items() if not k.startswith("_")} allowed["abs"] = abs allowed["round"] = round result = eval(expression, {"__builtins__": {}}, allowed) return f"计算结果:{result}" except Exception as e: return f"计算失败:{str(e)}"

工具定义有几个关键点。第一,docstring必须清晰描述工具的用途和参数,这是模型判断是否调用该工具的唯一依据。第二,工具函数必须处理异常,返回错误信息而不是抛出异常,否则整个流程会中断。第三,工具返回的内容要简洁,避免把大量原始数据塞回对话历史,浪费token。

实操心得:工具描述里最好加上“什么时候使用”的说明。比如搜索工具的描述里写了“当用户询问实时新闻时使用”,模型就能更准确地判断调用时机。我试过不加这句,模型经常在闲聊的时候也去搜索,浪费API调用。

3.4 节点函数实现:意图识别、搜索、总结

节点函数是流程的执行单元。我定义了三个节点:intent_node负责识别用户意图,search_node负责执行搜索,chat_node负责闲聊回复。

from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, AIMessage llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) def intent_node(state: AgentState): """识别用户意图,决定下一步走向""" last_message = state["messages"][-1].content prompt = f"""分析用户消息的意图,只返回以下三个词之一: - search:用户需要查询信息、新闻、数据 - chat:用户在进行闲聊、问候、感谢 - unknown:无法判断 用户消息:{last_message} 只返回一个词,不要有其他内容。""" response = llm.invoke([HumanMessage(content=prompt)]) intent = response.content.strip().lower() if intent not in ["search", "chat", "unknown"]: intent = "unknown" return {"user_intent": intent, "next_step": intent} def search_node(state: AgentState): """执行搜索并总结结果""" last_message = state["messages"][-1].content # 调用搜索工具 search_result = web_search.invoke({"query": last_message}) # 用模型总结搜索结果 summary_prompt = f"""基于以下搜索结果回答用户问题。如果搜索结果不相关,如实告知。 用户问题:{last_message} 搜索结果:{search_result} 请用简洁的中文回答,控制在200字以内。""" response = llm.invoke([HumanMessage(content=summary_prompt)]) return { "messages": [AIMessage(content=response.content)], "search_results": [search_result], "next_step": "end" } def chat_node(state: AgentState): """处理闲聊""" response = llm.invoke(state["messages"]) return {"messages": [response], "next_step": "end"}

意图识别节点用了零样本分类的思路,直接让模型输出一个词。这里有个细节:我在prompt里明确说了“只返回一个词”,但模型有时候还是会返回“search。”带标点,或者“search(搜索)”带括号。所以后面加了一层校验,如果返回值不在预期列表里,就归为unknown。这个兜底逻辑在实际运行中救了我好几次。

搜索节点里,我先调用搜索工具拿到原始结果,再用模型总结。为什么不直接把搜索结果返回给用户?因为原始搜索结果里有很多噪音,标题、URL、片段混在一起,直接返回体验很差。用模型总结一遍,输出会干净很多。但这里也有个权衡:总结会丢失一些细节,如果用户问的是具体数据,总结可能会把数字搞错。我的做法是,在总结的prompt里要求模型“保留关键数据”,并且在返回结果里附上原始搜索结果的引用。

3.5 图结构编排:条件边与循环控制

图结构的编排是LangGraph的核心。我用StateGraph定义流程,添加节点,然后用条件边决定流转方向。

from langgraph.graph import StateGraph, END # 创建图 workflow = StateGraph(AgentState) # 添加节点 workflow.add_node("intent", intent_node) workflow.add_node("search", search_node) workflow.add_node("chat", chat_node) # 设置入口点 workflow.set_entry_point("intent") # 添加条件边 def route_intent(state: AgentState): """根据意图决定下一步""" intent = state.get("user_intent", "unknown") if intent == "search": return "search" elif intent == "chat": return "chat" else: return "chat" # unknown 也走闲聊兜底 workflow.add_conditional_edges( "intent", route_intent, { "search": "search", "chat": "chat" } ) # 搜索和闲聊节点执行完后结束 workflow.add_edge("search", END) workflow.add_edge("chat", END) # 编译图 app = workflow.compile()

这个图的结构很简单:入口是intent节点,根据识别出的意图,走search或chat分支,两个分支执行完后都结束。但就是这么一个简单的图,已经比手动写的if-else清晰太多了。每个节点的职责单一,节点之间的流转条件显式定义,调试的时候一眼就能看出问题出在哪个环节。

注意:add_conditional_edges的第三个参数是一个映射字典,把route_intent的返回值映射到节点名称。这个映射必须完整,如果route_intent返回了一个字典里没有的值,运行时会报错。我刚开始用的时候忘了加unknown的映射,结果模型返回unknown的时候直接崩了。

3.6 运行与调试:从单轮到多轮对话

图编译好之后,就可以运行了。LangGraph的调用方式和LangChain的Chain类似,传入初始状态,返回最终状态。

def run_agent(user_input: str, history: list = None): """运行智能体""" if history is None: history = [] initial_state = { "messages": history + [HumanMessage(content=user_input)], "next_step": "", "search_results": [], "user_intent": "unknown" } result = app.invoke(initial_state) return result # 测试 result = run_agent("今天北京天气怎么样?") print(result["messages"][-1].content)

多轮对话的时候,只需要把上一轮返回的messages列表传进去,框架会自动维护对话历史。这里有个细节:add_messagesreducer会自动处理消息的去重和排序,你不需要手动管理消息的顺序。

调试的时候,我强烈建议开启LangGraph的流式输出和回调。流式输出可以让你看到每个节点的执行过程,回调可以记录每个节点的输入输出和耗时。这两个功能在排查问题时非常有用。

# 流式输出 for event in app.stream(initial_state): for node_name, node_output in event.items(): print(f"节点 {node_name} 输出:{node_output}")

4. 常见问题与排查技巧实录

4.1 工具调用失败:从报错到定位

工具调用失败是最常见的问题,表现五花八门:模型返回的JSON格式不对、调用了不存在的工具、工具参数类型不匹配、工具执行超时。排查的时候,我一般按这个顺序来:

第一,检查工具描述。模型是根据工具描述来决定是否调用、怎么调用的。如果描述模糊,模型就容易调错。比如一个工具叫process_data,描述是“处理数据”,模型根本不知道这个工具是干什么的,自然不会调用。改成extract_keywords,描述是“从文本中提取关键词,返回关键词列表”,模型就能准确判断调用时机。

第二,检查参数schema。LangChain会自动从函数签名生成参数schema,但如果你的参数类型是复杂对象,自动生成的schema可能不准确。这时候需要手动定义args_schema。我遇到过一个坑:工具参数是list[str],自动生成的schema里类型是array,但模型返回的是字符串,导致校验失败。后来手动指定了args_schema,问题就解决了。

第三,检查错误处理。工具函数内部一定要用try-except包裹,返回错误信息而不是抛出异常。如果工具抛出异常,整个图会中断,而且错误信息可能被框架吞掉,很难定位。

问题现象可能原因排查方法解决方案
模型不调用工具工具描述不清晰检查docstring是否说明用途补充“什么时候使用”的说明
工具调用参数错误参数schema不匹配打印模型返回的tool_calls手动定义args_schema
工具执行超时工具内部阻塞加日志记录执行时间设置超时、异步执行
工具结果格式错误返回值不是字符串检查函数返回类型统一返回字符串

4.2 状态丢失与消息重复:reducer的坑

状态管理最容易出的问题是消息重复和状态丢失。我遇到过一次:多轮对话的时候,模型总是重复上一轮的回答。排查了半天,发现是add_messagesreducer的问题。我在节点里返回了完整的messages列表,而不是只返回新增的消息,导致reducer把整个列表又追加了一遍。

正确的做法是:节点只返回新增的消息,reducer会自动追加到现有列表。如果你需要修改现有消息,用RemoveMessage或者update操作。

from langchain_core.messages import RemoveMessage def some_node(state: AgentState): # 正确:只返回新增消息 return {"messages": [AIMessage(content="新消息")]} # 错误:返回完整列表会导致重复 # return {"messages": state["messages"] + [AIMessage(content="新消息")]}

状态丢失的另一个常见原因是状态字段没有在TypedDict里定义。LangGraph只会维护TypedDict里声明的字段,如果你在节点里返回了一个未声明的字段,它会被忽略。我刚开始用的时候,在节点里返回了{"temp_data": ...},结果下一个节点读不到这个字段,排查了好久才发现是没在TypedDict里声明。

4.3 循环控制与终止条件:避免死循环

LangGraph支持循环,但循环如果没有正确的终止条件,就会变成死循环。我设计过一个“搜索-反思-再搜索”的循环:搜索节点执行完后,反思节点判断结果是否满意,如果不满意就回到搜索节点重新搜索。这个循环如果没有次数限制,模型可能会一直觉得“不满意”,无限循环下去。

我的做法是加一个计数器字段,每次循环加一,超过阈值就强制终止。

class AgentState(TypedDict): messages: Annotated[list, add_messages] loop_count: int max_loops: int def should_continue(state: AgentState): if state["loop_count"] >= state["max_loops"]: return "end" # 其他判断逻辑 return "continue"

实操心得:循环的终止条件最好有双重保险。一个是业务逻辑上的判断(比如“结果满意”),一个是硬性的次数限制。业务逻辑判断可能会因为模型的不稳定而失效,硬性限制是最后的兜底。

4.4 性能优化:从秒级到毫秒级的响应

智能体的响应速度是用户体验的关键。我做过统计,一个简单的搜索问答流程,如果不做优化,端到端响应时间在3-5秒左右。优化之后可以降到1-2秒。主要的优化手段有:

第一,并行执行。如果多个工具调用之间没有依赖关系,可以并行执行。LangGraph支持SendAPI,可以把多个任务分发到不同的节点并行执行。

第二,缓存。搜索结果、模型响应都可以缓存。我用functools.lru_cache缓存了搜索工具的调用,相同的查询直接返回缓存结果。模型响应也可以用LangChain的Cache机制缓存。

第三,流式输出。用户不需要等整个流程执行完才看到结果,可以边执行边输出。LangGraph的stream方法支持流式输出每个节点的结果,前端可以逐字显示。

第四,模型选择。不是所有节点都需要用大模型。意图识别这种简单任务,用gpt-4o-mini就够了,不需要上gpt-4o。总结节点可以用稍微大一点的模型,但也要权衡速度和质量的平衡。

优化手段优化前耗时优化后耗时实现难度
并行执行3.5s2.0s中等
缓存2.0s0.8s低
流式输出感知3.5s感知1.0s低
模型降级2.0s1.2s低

4.5 从单智能体到多智能体:扩展时的注意事项

当任务复杂度增加时,单智能体可能不够用,需要多个智能体协作。比如一个“研究报告生成”任务,可能需要一个搜索智能体、一个分析智能体、一个写作智能体。从单智能体扩展到多智能体的时候,有几个坑要注意。

第一,通信开销。智能体之间的消息传递会消耗token,如果智能体数量多、交互频繁,token消耗会急剧增加。我的做法是,尽量让智能体之间的通信简洁,只传递必要的信息,不要传递完整的对话历史。

第二,角色冲突。多个智能体如果有重叠的职责,可能会互相干扰。比如两个智能体都能搜索,它们可能会重复搜索相同的内容。解决方法是明确每个智能体的职责边界,用路由机制决定哪个任务交给哪个智能体。

第三,错误传播。一个智能体出错,可能会影响其他智能体。比如搜索智能体返回了错误结果,分析智能体基于错误结果进行分析,写作智能体又基于错误分析进行写作,错误会被放大。我的做法是,在每个智能体之间加校验节点,检查上一步的输出是否合理,不合理就触发重试或兜底。

def validation_node(state: AgentState): """校验上一步输出是否合理""" last_output = state["messages"][-1].content if len(last_output) < 10: return {"next_step": "retry"} if "错误" in last_output or "失败" in last_output: return {"next_step": "fallback"} return {"next_step": "continue"}

多智能体的编排比单智能体复杂得多,我的建议是先从单智能体开始,把流程跑通、把问题暴露出来,再根据实际需要拆分成多智能体。不要一上来就设计一个复杂的多智能体系统,那样调试成本太高,而且很多问题在单智能体阶段就能解决。

4.6 部署与监控:上线后的那些事

智能体上线之后,监控和日志是必不可少的。我主要关注几个指标:响应时间、token消耗、工具调用成功率、用户满意度。响应时间前面说过了,token消耗直接关系到成本,工具调用成功率反映了系统的稳定性,用户满意度可以通过反馈按钮收集。

日志方面,我记录了每个节点的输入输出、执行时间、错误信息。LangGraph的回调系统可以很方便地接入日志。我用LangSmith做追踪,每个请求的完整执行链路都能看到,排查问题的时候非常方便。

部署方式我试过两种:一种是FastAPI包装成HTTP服务,适合前后端分离的架构;一种是Streamlit直接做一个交互界面,适合快速演示。生产环境推荐FastAPI,因为它的并发性能更好,而且可以灵活地做鉴权、限流、缓存。

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): message: str history: list = [] @app.post("/chat") async def chat(request: ChatRequest): result = run_agent(request.message, request.history) return {"response": result["messages"][-1].content}

监控方面,我用了Prometheus收集指标,Grafana做可视化。关键指标包括:每分钟请求数、平均响应时间、P95响应时间、错误率、token消耗速率。这些指标能帮我快速发现异常,比如响应时间突然变长,可能是某个工具挂了;token消耗突然增加,可能是模型陷入了循环。

注意:智能体的输出是不确定的,同样的输入可能得到不同的输出。监控的时候不能只看单次请求的结果,要看统计趋势。比如工具调用成功率,单次失败可能是模型随机性导致的,但如果成功率持续下降,就说明有问题了。

5. 从框架开发中获得的经验与思考

5.1 框架不是银弹:什么时候该自己写

用了几个月框架之后,我最大的体会是:框架不是银弹。框架解决了状态管理、工具调用、流程编排这些通用问题,但它也引入了抽象层,增加了调试的复杂度。有时候一个简单的任务,用框架写反而比手动写更麻烦。

我的判断标准是:如果任务的流程是线性的、状态是简单的、工具调用是单轮的,手动写就够了。比如一个“翻译+润色”的流程,手动写两个函数串起来,比用LangGraph定义节点和边简单得多。但如果任务涉及多轮工具调用、条件分支、状态持久化,框架的优势就体现出来了。

另一个考虑因素是团队协作。如果团队里有多个人开发智能体,框架提供的标准化抽象能让代码更容易理解和维护。手动写的代码,每个人风格不同,维护成本很高。

5.2 智能体开发的“最后一公里”

智能体开发最难的其实不是技术,而是**“最后一公里”**:怎么让智能体稳定地处理各种边界情况。模型是概率性的,同样的输入可能得到不同的输出。工具调用可能失败,网络可能超时,用户输入可能超出预期。这些边界情况在demo阶段可能不会暴露,但上线之后会集中爆发。

我的经验是,把80%的时间花在边界情况的处理上。每个节点都要考虑:如果输入为空怎么办?如果工具返回错误怎么办?如果模型输出格式不对怎么办?如果超时怎么办?这些“如果”加起来,就是智能体稳定性的保障。

还有一个容易被忽视的点是用户预期管理。智能体不是万能的,它有时候会犯错。与其让用户自己发现错误,不如主动告知。比如在回答末尾加上“以上信息基于搜索结果,仅供参考”,或者当置信度低的时候主动说“我不太确定,建议您核实一下”。这种透明性反而能增加用户的信任。

5.3 后续可以扩展的方向

这个框架搭好之后,后续可以扩展的方向很多。一个是记忆的增强,目前只用了对话历史作为短期记忆,可以加入向量数据库做长期记忆,让智能体记住用户的偏好和历史交互。另一个是多模态,目前只处理文本,可以加入图片、语音的处理能力。还有一个是多智能体协作,把单智能体拆分成多个专职智能体,通过消息传递协作完成复杂任务。

工具生态的扩展也很重要。目前只有搜索和计算两个工具,可以加入数据库查询、API调用、文件操作等工具。工具越多,智能体的能力越强,但工具的管理和编排也会更复杂。我的建议是按需添加,不要一次性把所有工具都塞进去,那样模型反而不知道该用哪个。

最后再分享一个小技巧:定期回顾日志。我每周会花半小时看一遍智能体的执行日志,看看哪些工具调用失败了、哪些节点的耗时最长、哪些用户输入触发了兜底逻辑。这些日志里藏着优化的线索,比凭空想“哪里可以优化”有效得多。踩过的坑、修过的bug,都是这么一点点积累起来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 20:41:48

零基础自学PCB设计全流程:从建库、原理图到DRC与打样

简介&#xff1a;这份PDF文档面向PCB设计零基础学习者&#xff0c;也适合希望梳理嘉立创EDA操作习惯的入门者&#xff0c;围绕自学PCB的路径与项目实践展开。内容从创建工程与元件库、新建元件和查找封装&#xff0c;到绘制原理图、添加网络标签、模块整理与DRC检查&#xff0c…

作者头像 李华
网站建设 2026/11/3 20:41:46

OPENAI_API_KEY 报错排查:环境变量与统一 Key 通道

上周有位朋友在群里甩了一张终端截图&#xff0c;屏幕上就一行红字&#xff1a;Error: Missing environment variable \OPENAI_API_KEY。他说 Key 明明配了&#xff0c;echo也能打出来&#xff0c;可 Codex 就是不认。这种"看起来配了、实际上没配上"的状态&#xff…

作者头像 李华
网站建设 2026/11/3 20:41:44

AI如何重塑学术写作流程:从选题到排版的智能辅助

1. 项目概述&#xff1a;AI如何重塑学术写作流程第一次听说PaperZZ这个工具时&#xff0c;我正在指导本科生的毕业论文。看着学生熬夜改格式、反复调整目录的样子&#xff0c;突然意识到学术写作这个存在了数百年的传统流程&#xff0c;确实到了该被技术重构的时候。PaperZZ本质…

作者头像 李华
网站建设 2026/11/3 20:41:39

reverse-skill:以终为始的反向思维,高效解决复杂问题

"reverse-skill"这个词&#xff0c;我第一次认真琢磨它是在一次项目复盘会上。当时团队花了三个星期做的一个功能&#xff0c;上线后用户完全不买账&#xff0c;所有人都在讨论"下一步怎么优化"&#xff0c;只有一位老同事问了一句&#xff1a;"如果我…

作者头像 李华
网站建设 2026/11/3 20:41:35

从Claude Code到Pi:AI Coding工具链的偏好迁移与harness工程实践

1. 从"能跑就行"到"跑得省心"&#xff1a;AI Coding 工具链的偏好迁移 最近半年&#xff0c;我在几个开发者社群里反复看到同一个话题&#xff1a;有人把主力 AI Coding 工具从 Claude Code 换成了 Pi&#xff0c;而且换完之后普遍反馈"回不去了"…

作者头像 李华
网站建设 2026/11/3 20:40:28

小米MiMo-V2.6开源旗舰实测:API、RL对齐与3D效果差距

1. 凌晨那场发布&#xff0c;我盯完了全程小米选在凌晨发 MiMo-V2.6 系列&#xff0c;这个时间点本身就挺有意思。做模型发布的人都知道&#xff0c;凌晨发要么是算力排期卡到那个点&#xff0c;要么是想让海外和国内的技术圈在同一时间窗口里同时看到消息。我那天没睡&#xf…

作者头像 李华