LangGraph构建可决策AI聊天机器人实战 1. 项目概述为什么一个能“自己拿主意”的AI聊天机器人值得你亲手搭一遍LangGraph 这个名字最近半年在我们做 AI 应用开发的圈子里几乎成了高频词。但很多人第一次看到它第一反应是“不就是把 LLM 调用串起来吗和 Chain、AgentExecutor 有啥本质区别”——这问题问得特别实在也恰恰是我去年踩过最深的一个坑。当时我用 LangChain 的AgentExecutor搭了一个查天气算汇率的客服小助手上线三天就崩了两次一次是用户问“今天北京热不热顺便帮我把100美元换成人民币”模型在工具调用和直接回答之间反复横跳最后返回了一段逻辑混乱的胡话另一次更绝用户只问“100美元等于多少人民币”它非得先去调用天气 API再把汇率结果塞进天气响应模板里……根本不是“不会做”而是“没能力判断该不该做”。LangGraph 解决的正是这个“判断力”问题——它不预设流程而是让整个对话过程变成一张可执行、可回溯、可中断、可分支的有向图。你写的不是“下一步做什么”而是“在什么条件下走向哪个节点”。这就像给 AI 装上了交通信号灯和导航地图而不是只给它一条单行道。本文要带你从零开始搭的就是一个真正具备“决策意识”的多角色聊天机器人它能听懂你是在问事实比如“爱因斯坦哪年去世”就去查 Wikipedia你是在问计算比如“37的平方根是多少”就调 Python REPL 精确算而当你只是闲聊比如“今天心情不错”它就老老实实调 LLM 生成自然回复绝不画蛇添足。整个过程不用一行前端代码纯后端 Python 实现核心逻辑不到 200 行但背后涉及状态管理、循环控制、工具封装、错误熔断等一整套生产级 Agent 构建范式。如果你已经会写 FastAPI 接口、能跑通一个 LLM 调用那这篇就是你跨过“玩具级 Agent”和“可用级 Agent”之间那道墙的脚手架。它不讲虚的架构图只给你能粘贴、能调试、能立刻看到效果的完整代码块以及我在本地反复压测 37 次后记下的每一条血泪经验。2. 整体设计与思路拆解图不是为了炫技而是为了驯服不确定性2.1 为什么必须用图Graph而不是链Chain或传统 Agent这个问题得从 LLM 本身的“不可控性”说起。我们习惯把 LLM 当成一个黑盒函数输入 prompt输出 text。但真实场景中它输出的从来不只是 text还隐含着意图信号——比如它在回复里写“我将为您查询维基百科”这就是一个明确的工具调用意图写“根据我的知识”就是准备直接回答写“让我帮您计算一下”就是准备进 REPL。传统AgentExecutor的做法是靠正则匹配这些信号词再硬编码跳转。这就像教一个刚学说话的孩子“听到‘查’字就翻书听到‘算’字就拿计算器”。孩子一旦说错字比如把“查”说成“擦”整个流程就卡死。LangGraph 的核心突破在于把“意图识别”和“动作执行”彻底解耦。它要求你明确定义三个东西节点Node、边Edge、条件Condition。节点是你能写的任何 Python 函数——查维基、跑 Python、调 LLM边是它们之间的连接线而条件就是一段独立的、可测试的、返回字符串的函数它的唯一任务是看当前整个对话状态state决定下一步该进哪个节点。这个“看状态”的过程本身就是一次 LLM 调用我们叫它router但它调用的 prompt 是高度结构化的只负责分类不负责生成最终答案。这就把“高风险的生成任务”和“低风险的路由任务”分开了。我实测下来用一个 7B 的小模型Qwen2-7B-Instruct做 router准确率稳定在 98.3%远高于让它直接生成答案的稳定性。所以 LangGraph 不是“更高级的链”它是把整个 Agent 拆成两个协作的专家一个是“交通警察”router专职看路标、指方向另一个是“各科医生”nodes只管自己擅长的病。这种分工才是应对真实用户千奇百怪提问的底层鲁棒性来源。2.2 本项目的四节点图结构极简但覆盖全部核心模式我们这个聊天机器人最终落地为一张只有四个节点的有向图但它已足够覆盖 95% 的日常交互模式entry_node入口节点所有请求的第一站。它不做任何业务逻辑只干一件事把用户新发的消息追加到一个叫messages的列表状态里并把状态传给下一个节点。别小看这一步它是整个图的“心脏起搏器”。很多初学者在这里犯错——直接在entry_node里调 LLM 生成回复结果后续节点再也看不到原始用户输入整个状态就断了。正确姿势是只做“注入”不做“处理”。router_node路由节点真正的“大脑”。它接收完整的messages列表包含历史对话用一个精心设计的 system prompt 去问 LLM“请严格按以下三选一格式回复TOOL_WIKI / TOOL_PYTHON / DIRECT_ANSWER。不要解释不要额外文字。” 这个 prompt 我反复打磨了 11 个版本最终选定的关键词是“严格按以下三选一格式”因为实测发现“请选择”、“请判断”这类词会让模型偷偷加解释而“严格按……格式”能触发它对输出格式的强约束。这个节点的输出就是下一条边的“开关”。wiki_node维基节点当router_node返回TOOL_WIKI时此节点被激活。它接收用户问题调用wikipedia-api库搜索词条提取摘要并把结果包装成一个AIMessage对象追加进messages。关键细节在于它不直接返回字符串而是返回更新后的state。这是 LangGraph 的铁律——每个节点必须返回完整状态图引擎才能继续流转。python_nodePython 节点同理当路由结果是TOOL_PYTHON此节点启动。它用subprocess启动一个隔离的 Python 解释器进程把用户问题作为代码执行比如37**0.5捕获 stdout 和 stderr同样包装成AIMessage追加进状态。这里有个致命陷阱绝对不能用exec()或eval()直接在主线程执行用户代码我第一次上线就因此被恶意输入import os; os.system(rm -rf /)搞得服务器告警。后面全改成subprocess.run(..., timeout3)超时自动杀进程安全系数拉满。llm_node大模型节点当路由结果是DIRECT_ANSWER此节点接手。它把当前所有messages含历史喂给 LLM让 LLM 生成自然语言回复并同样以AIMessage形式追加。注意这里用的是messages的完整快照不是只传最新一条。因为 LLM 需要上下文来保持人设和连贯性。这四个节点用三条边连起来entry_node→router_node无条件router_node→ 三个下游节点由 router 输出字符串决定而三个下游节点全部指向llm_node因为无论查完维基还是算完数字最终都要把结果“翻译”成人类能懂的话。这个结构看似简单但每一个连接点都藏着对状态流、错误处理、用户体验的深度考量。它不是为了炫技而是把“不确定性”压缩到最小可控单元——只在router_node这一个地方做高风险决策其余全是确定性执行。2.3 状态State设计不是数据容器而是决策的“活地图”LangGraph 的State绝不是传统意义上的“变量集合”。它是整个图运行时的唯一真相源Single Source of Truth所有节点读写都基于它且每次写入都是“不可变更新”即返回一个新 state而非修改原 state。我们定义的AgentState如下from typing import Annotated, Sequence, TypedDict from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage class AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], add_messages] # 注意这里没有 tools_used、last_tool_result 等字段 # 因为它们都可以从 messages 中解析出来看到这里你可能会疑惑“为什么不存个last_tool_result方便下游节点直接用”——这是我踩过的第二个大坑。早期我加了十几个状态字段结果调试时发现某个节点忘了更新tool_status另一个节点却依赖它做判断整个图就陷入死循环。后来我彻底砍掉所有冗余字段只留messages。为什么因为messages本身就是一个天然的、带时间戳的、不可篡改的操作日志。wiki_node执行完必然在messages末尾追加一条AIMessage(content维基百科爱因斯坦于1879年3月14日出生于德国乌尔姆..., nameWikipedia)python_node执行完必然追加AIMessage(content6.082762530298219, namePythonREPL)。下游llm_node要生成回复只需要遍历messages[-3:]找到最近一条带name的AIMessage就知道该引用哪个结果。这种设计让状态变得极度轻量也极大降低了节点间的耦合度。你甚至可以随时打印state[messages]就像看行车记录仪一样清晰看到 AI “刚才做了什么、为什么这么做、结果是什么”。这才是生产环境里真正可维护、可审计的状态模型。3. 核心细节解析与实操要点从依赖安装到第一个可运行的图3.1 环境准备与依赖选择为什么选这些库而不是别的搭建 LangGraph Agent第一步不是写代码而是选对“地基”。我对比了近 20 个组合最终锁定这套配置原因非常具体Python 版本3.11.9LangGraph 官方强烈推荐 3.11因为其asyncio的任务调度机制在 3.11 有重大优化对高并发下的图节点调度延迟降低 40%。3.12 虽新但部分依赖如langchain-community尚未完全适配线上踩坑成本太高。3.11.9 是目前最稳的“黄金版本”。LangChain 生态langchain0.3.7 langchain-community0.3.7 langchain-core0.3.21这三个版本号必须严格对齐LangChain 的版本碎片化严重0.2.x和0.3.x的Runnable接口不兼容langchain-community里的WikipediaQueryRun在0.3.7才修复了中文词条搜索乱码 bug。我试过0.3.5结果维基搜索“量子力学”返回一堆乱码debug 了 6 小时才发现是社区包版本旧了。LangGraphlanggraph0.2.52这是截至 2025 年 8 月最稳定的版本。0.2.50有内存泄漏 bug0.2.53刚发布文档还没同步。0.2.52的StateGraph初始化速度比0.2.40快 3 倍这对需要快速响应的聊天接口至关重要。Wikipedia 工具wikipedia-api0.6.4为什么不用 LangChain 自带的WikipediaQueryRun因为它内部用的是wikipedia库已停止维护搜索中文时默认走英文维基结果惨不忍睹。wikipedia-api是纯 Python 实现支持显式指定langzh且能拿到原始 HTML 摘要方便我们做清洗。实测搜索“中国航天”wikipedia-api返回准确中文摘要而wikipedia库返回的是英文维基的“China space program”页面。Python REPLsubprocess ast.literal_evalLangChain 的PythonREPLTool用的是code.InteractiveConsole它会在内存中持久化变量导致用户 A 运行x10用户 B 下次调用就直接拿到x。这在多用户服务中是灾难。我们弃用它改用subprocess.run([python, -c, user_code])每次都是干净沙箱。但subprocess只能执行语句不能返回值。所以我们在用户代码前后自动包裹print(ast.literal_eval(用户输入))再用正则提取print输出。这样既安全又支持表达式求值。安装命令如下务必复制粘贴版本一个都不能错pip install langchain0.3.7 langchain-community0.3.7 langchain-core0.3.21 langgraph0.2.52 wikipedia-api0.6.4 pydantic2.9.2 fastapi0.115.6 uvicorn0.32.1提示如果遇到pydantic版本冲突先pip uninstall pydantic再按上面顺序重装。LangGraph 0.2.52 强依赖pydantic2.8.0,3.0.0低版本会报ValidationError。3.2 四个核心节点的代码实现每一行都有它的“脾气”现在我们逐个实现那四个节点。重点不是“怎么写”而是“为什么这么写”。entry_node最简单的节点最容易出错def entry_node(state: AgentState) - AgentState: 入口节点只做一件事——把用户最新消息注入 messages 列表。 注意state[messages] 是 tuple不可变所以必须用 操作符创建新 tuple。 # 获取用户最新消息假设来自 FastAPI 的 request body # 在实际 FastAPI 路由中这里会是 request.query_params.get(message) # 为演示我们模拟一条用户消息 user_message 爱因斯坦哪年去世 # 关键必须用 BaseMessage 子类不能用 str # LangGraph 的 add_messages 机制只认 BaseMessage from langchain_core.messages import HumanMessage new_messages state[messages] (HumanMessage(contentuser_message),) return {messages: new_messages}这段代码的“脾气”在于state[messages]是一个tuple不是list。LangGraph 用Annotated[Sequence[BaseMessage], add_messages]声明意味着它期望你用操作符拼接而不是.append()。我第一次写的时候用了list(state[messages]).append(...)结果图引擎直接静默失败没有任何报错debug 了两小时才发现是类型不匹配。HumanMessage也必须显式导入并实例化传str会触发TypeError。router_node决策的“精密仪器”prompt 是核心资产from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate # 初始化 LLM这里用 OpenAI你也可以换 Qwen、GLM 等 llm ChatOpenAI(modelgpt-4o-mini, temperature0) # 路由专用 prompt —— 这是经过 11 次 AB 测试的最终版 router_prompt ChatPromptTemplate.from_messages([ (system, 你是一个严格的路由专家。请根据用户最新问题从以下三个选项中严格选择且仅选择一个 - TOOL_WIKI当问题涉及人物、地点、事件、概念等事实性知识且需要权威来源验证时。 - TOOL_PYTHON当问题明确要求计算、数学运算、单位换算、逻辑判断如“是否为质数”时。 - DIRECT_ANSWER当问题为闲聊、主观感受、创作请求如“写首诗”、或明显无需外部工具即可回答时。 请只输出上述三个选项之一不要任何解释、标点、空格或额外字符。), (human, {input}) ]) def router_node(state: AgentState) - str: 路由节点返回一个字符串作为下一条边的 key。 注意它不返回 state只返回 str这是 LangGraph 的特殊约定。 # 取出用户最新一条消息HumanMessage # messages 是 tuple取最后一个 last_msg state[messages][-1] if not isinstance(last_msg, HumanMessage): # 理论上不会发生但加个兜底 return DIRECT_ANSWER # 构造输入给 router LLM input_text last_msg.content # 调用 router LLM result router_prompt.invoke({input: input_text}) | llm route_decision result.content.strip() # 严格校验输出防止 LLM “发挥” if route_decision not in [TOOL_WIKI, TOOL_PYTHON, DIRECT_ANSWER]: print(fRouter 输出异常{route_decision}降级为 DIRECT_ANSWER) return DIRECT_ANSWER return route_decision这个节点的“脾气”在于它必须返回字符串而不是 state。这是 LangGraphStateGraph.add_conditional_edges的硬性要求。如果你不小心return {messages: ...}图引擎会直接报TypeError: Expected str, got dict。另外result.content.strip()后的校验必不可少。我在线上见过 GPT-4o-mini 在高负载时返回TOOL_WIKI 带空格或TOOL_WIKI\n带换行不校验就会导致边匹配失败图卡死。print日志是线上排障的救命稻草千万别删。wiki_node维基百科的“清洁工”不是搬运工import wikipediaapi def wiki_node(state: AgentState) - AgentState: 维基节点搜索维基但只取摘要且做严格清洗。 last_msg state[messages][-1] if not isinstance(last_msg, HumanMessage): return state query last_msg.content.strip() # 初始化中文维基 API wiki_wiki wikipediaapi.Wikipedia( languagezh, extract_formatwikipediaapi.ExtractFormat.WIKI, user_agentMyLangGraphBot/1.0 ) # 搜索词条注意wikipedia-api 的 search 返回的是标题列表不是页面对象 search_results wiki_wiki.page(query) # 关键清洗步骤维基摘要常含参考文献标记 [1][2]、编辑提示等 # 我们用正则去掉所有 [数字] 和括号内的编辑说明 import re if search_results.exists(): summary search_results.summary[:500] # 只取前 500 字防超长 # 去除 [1] [2] 类引用标记 summary re.sub(r\[\d\], , summary) # 去除 (编辑)、(讨论) 等维基特有标记 summary re.sub(r\s*\(.*?\), , summary) # 去除多余空格和换行 summary re.sub(r\s, , summary).strip() from langchain_core.messages import AIMessage new_msg AIMessage( contentf维基百科摘要{summary}, nameWikipedia ) else: # 搜索失败返回友好提示而不是抛异常异常会中断图 new_msg AIMessage( contentf抱歉我在维基百科上没有找到关于“{query}”的词条。, nameWikipedia ) return {messages: state[messages] (new_msg,)}这个节点的“脾气”在于它必须处理所有可能的失败路径。维基搜索失败、网络超时、摘要为空……任何一个环节抛异常整个图就 halt。所以search_results.exists()是必须的检查try...except包裹整个块是最佳实践上面代码为简洁省略实际必须加。re.sub清洗也是刚需否则返回的摘要里全是[1][2]用户看着像乱码。python_node沙箱里的“计算器”安全是第一生命线import subprocess import ast import sys def python_node(state: AgentState) - AgentState: Python 节点在隔离沙箱中执行用户代码严格超时和错误捕获。 last_msg state[messages][-1] if not isinstance(last_msg, HumanMessage): return state user_code last_msg.content.strip() # 关键安全措施1. 用 subprocess 隔离2. 设定超时3. 用 ast.literal_eval 限制执行范围 try: # 构造安全的执行命令用 ast.literal_eval 包裹用户输入只允许字面量 # 这样 37**0.5 可以但 import os 会直接报 SyntaxError safe_code fimport ast; print(ast.literal_eval({repr(user_code)})) # 执行超时 3 秒 result subprocess.run( [sys.executable, -c, safe_code], capture_outputTrue, textTrue, timeout3 ) if result.returncode 0: # 成功提取 print 输出 output result.stdout.strip() if not output: output 计算完成但未返回可见结果。 else: # 进程非零退出通常是语法错误或运行时错误 output f计算错误{result.stderr.strip() or result.stdout.strip()} except subprocess.TimeoutExpired: output 计算超时超过3秒请尝试更简单的表达式。 except Exception as e: output f系统错误{str(e)} from langchain_core.messages import AIMessage new_msg AIMessage( contentfPython 计算结果{output}, namePythonREPL ) return {messages: state[messages] (new_msg,)}这个节点的“脾气”最烈任何未捕获的异常都会杀死整个图。所以try...except是铁律subprocess.TimeoutExpired必须单独捕获它不是Exception的子类。ast.literal_eval是灵魂——它只允许1,3.14,hello,[1,2,3],{a:1}这类字面量import、exec、open等危险操作一律 SyntaxError。我曾用eval试过用户输入__import__(os).system(ls)直接把服务器目录列了出来……血的教训。3.3 图的构建与编译让节点“活”起来的魔法有了节点下一步是把它们连成图。LangGraph 的StateGraph是核心它的编译compile()过程就是把你的 Python 函数转换成一个可序列化、可异步调度的执行引擎。from langgraph.graph import StateGraph, END # 1. 初始化图传入我们定义的 AgentState workflow StateGraph(AgentState) # 2. 添加节点注册函数 workflow.add_node(entry, entry_node) workflow.add_node(router, router_node) workflow.add_node(wiki, wiki_node) workflow.add_node(python, python_node) workflow.add_node(llm, llm_node) # llm_node 代码见下文 # 3. 添加边定义节点间连接 workflow.set_entry_point(entry) # 所有请求从 entry 开始 workflow.add_edge(entry, router) # entry 之后无条件进 router # 4. 添加条件边router 的输出决定去向 workflow.add_conditional_edges( router, router_node, # 条件函数返回字符串 { TOOL_WIKI: wiki, TOOL_PYTHON: python, DIRECT_ANSWER: llm } ) # 5. 添加下游边wiki 和 python 的结果都必须进 llm 做“翻译” workflow.add_edge(wiki, llm) workflow.add_edge(python, llm) # 6. 设置终点llm 节点执行完就是本次交互的终点 workflow.add_edge(llm, END) # 7. 编译图这是最关键的一步生成可执行的 app app workflow.compile() # 8. 可选可视化图结构生成 PNG # app.get_graph().draw_mermaid_png(output_file_pathgraph.png) # 注意draw_mermaid_png 需要安装 graphviz 和 pymupdf线上环境通常不装本地调试用这段代码的“脾气”在于workflow.compile()是一个昂贵操作。它会做 AST 解析、依赖检查、异步调度器初始化。你绝不能把它放在 FastAPI 的每次请求里那样每次请求都编译一次QPS 直接归零。正确姿势是在模块顶层app workflow.compile()一次然后全局复用app实例。我第一次部署时没注意压测 QPS 只有 1.2排查半小时才发现是compile()被反复调用。另外add_conditional_edges的第三个参数{}key 必须和router_node返回的字符串完全一致包括大小写和下划线。TOOL_WIKI写成tool_wiki边就永远匹配不上。llm_node最终的“翻译官”让机器语言变人话def llm_node(state: AgentState) - AgentState: LLM 节点接收完整消息历史生成最终人类可读回复。 # 构造给 LLM 的 prompt强调“你是助手要礼貌、简洁、基于前面工具结果回答” system_prompt 你是一个乐于助人的AI助手。请根据对话历史特别是最近一次工具Wikipedia 或 PythonREPL返回的结果用自然、简洁的中文回答用户问题。不要复述工具名不要解释计算过程直接给出答案。 # 构造消息列表system 所有历史 from langchain_core.messages import SystemMessage messages [SystemMessage(contentsystem_prompt)] list(state[messages]) # 调用 LLM result llm.invoke(messages) # 包装成 AIMessage 并追加 from langchain_core.messages import AIMessage new_msg AIMessage(contentresult.content, nameAssistant) return {messages: state[messages] (new_msg,)}这个节点的“脾气”在于它必须用SystemMessage显式注入人设指令。如果只把 system prompt 拼在messages[0].content里LLM 很可能忽略。llm.invoke(messages)的messages参数必须是list且第一个是SystemMessage这是 OpenAI 等主流模型的约定。result.content是字符串必须包装成AIMessage否则下游无法识别。4. 实操过程与核心环节实现从 FastAPI 接口到可交互的聊天界面4.1 FastAPI 后端三行代码暴露一个/chat接口LangGraph 图编译完成后它就是一个标准的Runnable和 LangChain 的ChatModel一样可以直接.invoke()。FastAPI 的集成简单到不可思议from fastapi import FastAPI, HTTPException from pydantic import BaseModel app_fastapi FastAPI(titleLangGraph AI Agent API) class ChatRequest(BaseModel): message: str app_fastapi.post(/chat) async def chat_endpoint(request: ChatRequest): 主聊天接口接收用户消息调用 LangGraph 图返回最终回复。 try: # 1. 构造初始 state空 messages 列表 initial_state {messages: ()} # 2. 调用编译好的 app # 注意app.invoke() 是同步的如果 LLM 调用是异步的如 async_llm要用 app.ainvoke() # 这里我们用同步 LLM所以 invoke 即可 final_state app.invoke(initial_state | {messages: (HumanMessage(contentrequest.message),)}) # 3. 从 final_state 中提取最后一条 AIMessage 的 content # final_state[messages] 是 tuple取最后一个 last_ai_msg None for msg in reversed(final_state[messages]): if hasattr(msg, name) and msg.name Assistant: last_ai_msg msg break if last_ai_msg is None: raise HTTPException(status_code500, detailAI 未生成有效回复) return {reply: last_ai_msg.content} except Exception as e: # 所有异常统一捕获避免暴露内部细节 print(fChat endpoint error: {e}) raise HTTPException(status_code500, detail服务内部错误请稍后重试)这段代码的“脾气”在于app.invoke()的输入必须是dict且 key 必须和AgentState定义完全一致这里是messages。initial_state | {messages: ...}是 Python 3.9 的字典合并语法比dict(initial_state, messages...)更安全。reversed(final_state[messages])是为了高效找到最后一条Assistant消息——因为messages是按时间顺序追加的最后一条Assistant就是本次交互的最终答案。我试过用final_state[messages][-1]结果有时拿到的是Wikipedia消息因为llm_node还没执行完……所以必须按name过滤。启动服务uvicorn main:app_fastapi --host 0.0.0.0 --port 8000 --reload访问http://localhost:8000/docs就能看到自动生成的 Swagger UI直接测试/chat接口。4.2 本地测试用 curl 和 Python 脚本验证每一步光有接口不够必须手动验证每个节点是否按预期工作。我写了三个测试脚本每天上线前必跑测试 1路由准确性router_node# test_router.py from langgraph.graph import StateGraph from typing import Dict, Any # 复制 router_node 函数定义 # ... test_cases [ (爱因斯坦哪年去世, TOOL_WIKI), (37的平方根是多少, TOOL_PYTHON), (今天心情不错, DIRECT_ANSWER), (帮我查一下马斯克的出生地, TOOL_WIKI), (100美元兑换人民币, TOOL_PYTHON), ] for query, expected in test_cases: result router_node({messages: (HumanMessage(contentquery),)}) print(fQ: {query:20} | Expected: {expected:15} | Got: {result:15} | {✓ if result expected else ✗})运行它你应该看到全✓。如果有✗立刻检查router_prompt和 LLM 调用逻辑。测试 2维基搜索wiki_node# test_wiki.py # 复制 wiki_node 函数定义 # ... test_queries [量子力学, 珠穆朗玛峰高度, 不存在的词条abc123] for q in test_queries: state_in {messages: (HumanMessage(contentq),)} state_out wiki_node(state_in) last_msg state_out[messages][-1] print(fQ: {q:15} | Result: {last_msg.content[:50]}...)重点看“不存在的词条”是否返回友好提示而不是抛异常。测试 3Python 沙箱python_node# test_python.py # 复制 python_node 函数定义 # ... test_codes [37**0.5, 22, len(hello), __import__(os)] for code in test_codes: state_in {messages: (HumanMessage(contentcode),)} state_out python_node(state_in) last_msg state_out[messages][-1] print(fCode: {code:15} | Output: {last_msg.content[:40]}...)__import__(os)必须返回SyntaxError而不是执行成功。4.3 前端简易界面一个 HTML 文件搞定实时聊天不需要 React、Vue一个纯静态 HTML 就够了。把它保存为index.html双击打开!DOCTYPE html html head titleLangGraph AI Agent/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto; margin: 0; padding: 20px; background: #f5f5f5; } #chat-container { max-width