前端转agent-【python】-08 用 LangGraph 把 Agent 做成状态机:像写 Vue 3 状态管理一样编排 AI 流程

前端转agent-【python】-08 用 LangGraph 把 Agent 做成状态机:像写 Vue 3 状态管理一样编排 AI 流程

如果你写过复杂的前端交互,一定用过状态管理------Vuex、Pinia、甚至是组合式函数里的 reactive + computed 来驱动 UI 流转。

今天用 Python 的 LangGraph 把 AI Agent 也变成一张"图"------每个节点是一个操作,边是条件分支。

我们用本地模型 Qwen3:4b 跑一个带决策的客服机器人,看看状态图怎么让 Agent 变得像你熟悉的 Vue 3 状态管理一样可控。

为什么需要 LangGraph?

一般的 Agent 写法是写一个 while 循环,手动拼装 messages,调用 LLM,解析输出,硬编码一堆 if-else。当逻辑变复杂(需要重试、分支路由、工具调用循环)时,代码很快就变成意大利面条。

LangGraph 的核心思想 :把 Agent 的工作流建模成一张有向图 ------节点是执行步骤(调用 LLM、处理输入、返回结果),边是状态转移(带条件判断)。

这就像你在 Vue 3 里用 Pinia组合式 API 管理一个多步骤向导:当前状态驱动下一步往哪走,每个步骤对应一个处理逻辑,状态一改,界面自动更新。

Vue 3 视角:LangGraph ≈ 用 Pinia 定义一个带条件跳转的流程 store,只不过节点里运行的是 LLM 调用而不是组件渲染。

环境准备

bash 复制代码
pip install langgraph ollama

确保模型已拉取(注意是 4b,稍大一点,但仍然本地可跑):

bash 复制代码
ollama pull qwen3:4b

第一个状态图:极简聊天机器人

我们先写一个最基本的"接收消息 → 调模型 → 返回回复"的图,感受一下结构。

python 复制代码
# simple_graph_chat.py
import ollama
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END

MODEL = "qwen3:4b"

# 1. 定义状态(State),就像你在 Pinia 里定义的 state 类型
class ChatState(TypedDict):
    messages: Annotated[list, "对话历史"]
    user_input: str
    response: str

# 2. 定义节点(Node),每个节点是一个纯函数,接收 state 返回部分更新的 state
def call_llm(state: ChatState) -> dict:
    """调用 LLM,并把结果写入 response 字段"""
    messages = state["messages"] + [
        {"role": "user", "content": state["user_input"]}
    ]
    result = ollama.chat(model=MODEL, messages=messages)
    return {"response": result["message"]["content"]}

def prepare_next_turn(state: ChatState) -> dict:
    """把本轮的问答追加到历史里,为下一轮做准备"""
    updated_messages = state["messages"] + [
        {"role": "user", "content": state["user_input"]},
        {"role": "assistant", "content": state["response"]}
    ]
    return {"messages": updated_messages}

# 3. 构建图
graph = StateGraph(ChatState)

graph.add_node("llm", call_llm)             # 添加节点
graph.add_node("history", prepare_next_turn)

graph.set_entry_point("llm")                # 入口
graph.add_edge("llm", "history")            # llm → history
graph.add_edge("history", END)              # history → 结束

app = graph.compile()                       # 编译成可运行的应用

# 4. 运行一次对话
state = {"messages": [], "user_input": "你好,我叫小明"}
result = app.invoke(state)
print("🤖:", result["response"])
print("📝 当前历史:", len(result["messages"]), "条")

输出:

makefile 复制代码
🤖: 你好小明!有什么可以帮你的?
📝 当前历史: 2 条

Vue 3 对比StateGraph 就像你定义一个 useAgentFlow 的组合式函数,statereactive 对象,add_node 是添加处理函数,add_edge 是指定下一步操作。invoke() 相当于执行一次流程,并返回更新后的状态。

进阶:带条件分支的客服 Agent

真实场景里,我们需要根据用户意图走不同分支------比如"问产品"走知识库,"投诉"走安抚流程,"其他"走通用问答。

这正适合用 LangGraph 的条件边(conditional edges)。

场景设计

  • 意图分类 :让 LLM 判断用户输入是 productcomplaint 还是 general
  • 产品节点:回答产品相关问题(模拟固定回复)。
  • 投诉节点:安抚用户并转人工(打印一条消息)。
  • 通用节点:调用 LLM 自由回复。

代码实现

python 复制代码
# customer_service_agent.py
import ollama
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END

MODEL = "qwen3:4b"

class AgentState(TypedDict):
    messages: Annotated[list, "完整对话历史"]
    user_input: str
    intent: str
    response: str

# ---- 意图分类节点 ----
def classify_intent(state: AgentState) -> dict:
    """让 LLM 判断意图,输出三个类别之一"""
    prompt = f"""你是一个客服意图分类器。用户消息是:
"{state['user_input']}"
请只输出以下三个单词之一:product, complaint, general。
不要输出任何其他内容。"""
    result = ollama.generate(model=MODEL, prompt=prompt)
    intent = result["response"].strip().lower()
    # 简单容错
    if intent not in ["product", "complaint", "general"]:
        intent = "general"
    return {"intent": intent}

# ---- 产品咨询节点 ----
def product_handler(state: AgentState) -> dict:
    response = "感谢您的产品咨询!我们的智能手表续航可达7天,支持IP68防水。请问还有什么需要了解的吗?"
    return {"response": response}

# ---- 投诉节点 ----
def complaint_handler(state: AgentState) -> dict:
    response = "非常抱歉给您带来不便。我们已经记录下您的问题,客服代表将在2小时内联系您。"
    return {"response": response}

# ---- 通用聊天节点 ----
def general_handler(state: AgentState) -> dict:
    messages = state["messages"] + [{"role": "user", "content": state["user_input"]}]
    result = ollama.chat(model=MODEL, messages=messages)
    return {"response": result["message"]["content"]}

# ---- 历史更新节点 ----
def update_history(state: AgentState) -> dict:
    updated = state["messages"] + [
        {"role": "user", "content": state["user_input"]},
        {"role": "assistant", "content": state["response"]}
    ]
    return {"messages": updated}

# ---- 路由函数(条件边) ----
def route_by_intent(state: AgentState) -> Literal["product", "complaint", "general"]:
    return state["intent"]

# ---- 构建状态图 ----
builder = StateGraph(AgentState)

builder.add_node("classify", classify_intent)
builder.add_node("product", product_handler)
builder.add_node("complaint", complaint_handler)
builder.add_node("general", general_handler)
builder.add_node("update_history", update_history)

builder.set_entry_point("classify")

# 条件边:根据 intent 字段走不同分支
builder.add_conditional_edges(
    "classify",
    route_by_intent,
    {
        "product": "product",
        "complaint": "complaint",
        "general": "general"
    }
)

# 所有处理节点结束后都去更新历史
builder.add_edge("product", "update_history")
builder.add_edge("complaint", "update_history")
builder.add_edge("general", "update_history")
builder.add_edge("update_history", END)

agent = builder.compile()

# ---- 测试 ----
if __name__ == "__main__":
    state = {"messages": [], "user_input": ""}
    
    print("🤖 智能客服 (product / complaint / general)")
    while True:
        user = input("\n🧑 你: ")
        if user == "/bye":
            break
        state["user_input"] = user
        state = agent.invoke(state)
        print(f"🎯 意图: {state['intent']}")
        print(f"🤖 客服: {state['response']}")

运行效果:

erlang 复制代码
🧑 你: 你们的表防水吗?
🎯 意图: product
🤖 客服: 感谢您的产品咨询!我们的智能手表续航可达7天...

🧑 你: 我的订单三天还没发货,太慢了!
🎯 意图: complaint
🤖 客服: 非常抱歉给您带来不便。我们已经记录下您的问题...

🧑 你: 今天天气不错
🎯 意图: general
🤖 客服: 是啊,阳光明媚,很适合出去走走!

Vue 3 开发者横向对比

如果你在 Vue 3 里用 Pinia 管理类似的多分支流程,核心逻辑会是这样的:

typescript 复制代码
// useAgentStore.ts
import { defineStore } from 'pinia';
import { reactive, computed } from 'vue';

interface AgentState {
  messages: Array<{ role: string; content: string }>;
  userInput: string;
  intent: 'product' | 'complaint' | 'general' | '';
  response: string;
}

export const useAgentStore = defineStore('agent', () => {
  const state = reactive<AgentState>({
    messages: [],
    userInput: '',
    intent: '',
    response: ''
  });

  // 意图分类(模拟异步 LLM 调用)
  async function classifyIntent() {
    const intent = await callLLMClassify(state.userInput); // 伪代码
    state.intent = intent;
  }

  // 各分支处理函数
  function handleProduct() {
    state.response = '产品回复...';
  }
  function handleComplaint() {
    state.response = '投诉安抚...';
  }
  function handleGeneral() {
    state.response = '通用聊天...';
  }
  function updateHistory() {
    state.messages.push(
      { role: 'user', content: state.userInput },
      { role: 'assistant', content: state.response }
    );
  }

  // 主流程:可以根据 intent 的计算属性来触发下一步(这里用 watch 或手动调用)
  async function runFlow(userInput: string) {
    state.userInput = userInput;
    await classifyIntent();
    // 根据 intent 分发
    if (state.intent === 'product') handleProduct();
    else if (state.intent === 'complaint') handleComplaint();
    else handleGeneral();
    updateHistory();
  }

  return { state, runFlow };
});

在组件中使用:

vue 复制代码
<template>
  <div>
    <p v-for="msg in agent.state.messages" :key="msg.content">
      {{ msg.role }}: {{ msg.content }}
    </p>
    <input v-model="userInput" @keyup.enter="send" />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useAgentStore } from './useAgentStore';

const agent = useAgentStore();
const userInput = ref('');

async function send() {
  await agent.runFlow(userInput.value);
  userInput.value = '';
}
</script>

本质上,LangGraph 的 StateGraph 就是把 Pinia 里的 状态、动作、条件分发 抽象成了图节点和边。

区别在于 :Pinia 的流程靠手动调用函数和 if/else 组织;而 LangGraph 通过声明式图结构,让复杂的条件跳转、循环、错误重试更清晰,也更容易可视化。

为什么用状态图做 Agent?

  1. 可视化:LangGraph 可以导出图结构,团队沟通更清晰。
  2. 可测试:每个节点是纯函数(或接近纯),单独测试。
  3. 容错与重试:可以加错误处理节点,形成循环,比如"分类失败 → 重试"。
  4. 强制结构:避免手写 while 循环导致的状态不可预测。
  5. 前端友好:如果你熟悉 Vue 的响应式状态管理和 Pinia 的 action 编排,那么理解 LangGraph 的状态图只有一步之遥。

更进一步:加入循环与工具调用

LangGraph 天生支持循环(工具调用 Agent 的标准模式)。例如,加入一个"搜索知识库"的工具节点,LLM 决定是否要调用工具,然后回到 LLM 节点,直到生成最终答案。

这就是 OpenAI 的 function calling 循环模式,LangGraph 用几张图就能搭出来。

python 复制代码
# tool_loop_agent.py
"""
带工具调用循环的 Agent 示例(独立文件)
演示 LangGraph 的"LLM ⇄ 工具"循环模式
"""
import ollama
import json
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END

MODEL = "qwen3:4b"

# ----- 状态定义 -----
class ToolAgentState(TypedDict):
    messages: Annotated[list, "对话历史"]
    user_input: str
    tool_call: dict | None
    tool_result: str
    final_answer: str

# ----- 工具定义(示例) -----
def get_weather(city: str) -> str:
    """模拟天气查询"""
    weather_db = {
        "北京": "晴天,25°C",
        "上海": "小雨,22°C",
        "深圳": "多云,28°C",
    }
    return weather_db.get(city, f"未找到{city}的天气信息")

def calculator(expression: str) -> str:
    """安全计算数学表达式"""
    try:
        allowed = set("0123456789+-*/().% ")
        if not all(c in allowed for c in expression):
            return "错误:包含非法字符"
        return str(eval(expression))
    except Exception as e:
        return f"计算错误:{str(e)}"

TOOLS = {
    "get_weather": (get_weather, {"city": "str"}),
    "calculator": (calculator, {"expression": "str"}),
}

SYSTEM_PROMPT = """你是一个能使用工具的助手。可用工具:
- get_weather(city): 查天气
- calculator(expression): 算数学表达式
需要工具时输出 JSON:{"tool": "工具名", "args": {...}}
不需要工具时直接回答。"""

# ----- 节点函数 -----
def call_llm(state: ToolAgentState) -> dict:
    """LLM 思考节点"""
    messages = [{"role": "system", "content": SYSTEM_PROMPT}] + state["messages"] + [
        {"role": "user", "content": state["user_input"]}
    ]
    response = ollama.chat(model=MODEL, messages=messages)
    content = response["message"]["content"].strip()

    # 尝试解析工具调用
    try:
        tool_call = json.loads(content)
        if "tool" in tool_call and "args" in tool_call:
            return {"tool_call": tool_call}
    except:
        pass
    return {"final_answer": content}

def execute_tool(state: ToolAgentState) -> dict:
    """执行工具节点"""
    tc = state["tool_call"]
    tool_name = tc["tool"]
    args = tc["args"]

    func, _ = TOOLS[tool_name]
    if tool_name == "get_weather":
        result = func(args["city"])
    elif tool_name == "calculator":
        result = func(args["expression"])
    else:
        result = "未知工具"

    # 将工具执行结果加入历史
    new_messages = state["messages"] + [
        {"role": "user", "content": state["user_input"]},
        {"role": "assistant", "content": f"调用工具:{tool_name}({json.dumps(args)})"},
        {"role": "tool", "content": result}
    ]
    return {
        "messages": new_messages,
        "tool_result": result,
        "tool_call": None  # 清除,防止再次触发
    }

def should_use_tool(state: ToolAgentState) -> Literal["tool", "end"]:
    if state.get("tool_call"):
        return "tool"
    return "end"

# ----- 构建图 -----
builder = StateGraph(ToolAgentState)
builder.add_node("llm", call_llm)
builder.add_node("tools", execute_tool)

builder.set_entry_point("llm")
builder.add_conditional_edges("llm", should_use_tool, {
    "tool": "tools",
    "end": END
})
builder.add_edge("tools", "llm")  # 工具结果返回 LLM 继续思考

agent = builder.compile()

# ----- 运行 -----
if __name__ == "__main__":
    state = {"messages": [], "user_input": "", "tool_call": None, "tool_result": "", "final_answer": ""}
    print("🤖 带工具循环的 Agent (输入 /bye 退出)")
    while True:
        user = input("\n🧑 你: ")
        if user == "/bye":
            break
        state["user_input"] = user
        state["tool_call"] = None
        state["final_answer"] = ""
        final_state = agent.invoke(state)
        print(f"🤖 Agent: {final_state.get('final_answer', '无响应')}")
        state["messages"] = final_state["messages"]
bash 复制代码
## 运行示例

启动代理后,可以尝试以下对话,观察工具调用循环的执行过程:

🧑 你: 北京今天天气怎么样?  
🤖 Agent: 北京今天晴天,25°C

🧑 你: 计算 123 * 456  
🤖 Agent: 123 * 456 = 56088

🧑 你: 深圳天气如何?顺便算一下 78 + 22  
🤖 Agent: 深圳天气多云,28°C;78 + 22 = 100


在内部,LangGraph 自动经历了以下循环:

1. LLM 收到 "深圳天气如何?顺便算一下 78 + 22",判断需要两个工具,先输出 `{"tool": "get_weather", "args": {"city": "深圳"}}` → 执行工具 → 返回天气结果
2. 工具结果返回给 LLM,LLM 发现还需要计算,输出 `{"tool": "calculator", "args": {"expression": "78 + 22"}}` → 执行工具 → 返回计算结果
3. LLM 再次收到结果,综合输出最终回答

整个过程无需手动编写 while 循环,完全由状态图的 `conditional_edges` 自动驱动。

如果放到 Vue 3 的思维里,这就像一个 Pinia store 里有 llmActiontoolAction,用 watch 监听状态变化,决定是继续调用工具还是结束,形成一个"请求-判断-再请求"的闭环。

总结

  • LangGraph 把 Agent 抽象成状态图,状态 = 对话上下文 + 中间结果节点 = 处理函数边 = 流转规则
  • 用 Ollama + Qwen3:4b 本地跑,无需外部 API,成本零。
  • 对 Vue 3 开发者来说,LangGraph 就像是把 Pinia 的 action 编排和条件分发变成了声明式的图,心智模型高度一致。
  • 从简单的顺序聊天,到复杂的分支路由、工具循环,状态图都能优雅表达。

想试试?复制上面的 customer_service_agent.py,运行起来,感受一下"画图即编程"的 Agent 开发体验。

bash 复制代码
python customer_service_agent.py
相关推荐
刺猬的温驯3 小时前
语音克隆模型的难点之一:音素对齐及交叉注意力早期失效问题 (兼论旋转位置编码)——F5-TTS、SupertonicTTS、VoxFlash-TTS 对比
人工智能·语音合成·tts
道友可好3 小时前
AI 是最好的混乱放大器:代码熵管理实战
前端·人工智能·后端
不加辣椒5 小时前
第7章 边界与约束技术:确保输出的准确性与安全性
人工智能
AI悦创Python辅导5 小时前
Claude Code 越用越乱?Sub-Agents 才是上下文污染的解法
人工智能
Bigfish_coding5 小时前
前端转agent-【python】-07 长期记忆进阶:用 ChromaDB + 语义搜索给 Agent 装上真正的长期记忆
人工智能
阿黎梨梨5 小时前
AI Loop:告别“人肉写提示词”,让代码替你“鞭策”AI
javascript·人工智能
甲维斯6 小时前
坦克大战测试全翻车了!豆包,DeepSeek,Qwen,GPT,Claude
前端·人工智能·游戏开发
若丶相见6 小时前
AI 大模型零基础知识扫盲
人工智能
猿人谷8 小时前
不只是 CPU 阈值:STAR 如何用 GAT + Transformer 做容器级自动扩缩容?
人工智能·算法