把新版 LangChain Agent 做成真正应用:Memory、Streaming、SSE 与 Structured Output

前两篇解决了:

复制代码
Agent 怎么执行
Tool 怎么调用
State 怎么管理
Context 怎么注入
Middleware 怎么扩展

但到这里,Agent 仍然有几个很明显的问题:

复制代码
不会自动记住上一轮对话
用户必须等最终答案一次性返回
前端不知道 Agent 正在干什么
最终输出还是自由文本

这一篇我们把它真正升级成一个比较完整的 AI 后端服务。

核心包括:

vbnet 复制代码
Checkpointer
thread_id
短期记忆
Streaming
Tool Custom Events
FastAPI SSE
Structured Output

一、为什么 Agent 默认没有真正的多轮记忆?

第一次:

复制代码
用户:
我叫猪头

FastAPI 调:

py 复制代码
agent.ainvoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "我叫猪头"
            }
        ]
    }
)

第二次:

复制代码
我叫什么?

如果你又只传:

py 复制代码
{
    "messages": [
        {
            "role": "user",
            "content": "我叫什么?"
        }
    ]
}

那么第二次 Agent 根本不知道第一次发生了什么。

因为:

vbscript 复制代码
HTTP Request 1
结束

HTTP Request 2
重新开始

所以需要:

复制代码
Checkpointer

二、Checkpointer 是什么?

可以直接记:

ini 复制代码
State
=
保存什么

Checkpointer
=
怎么保存和恢复

thread_id
=
保存的是哪一个会话

开发阶段可以:

py 复制代码
from langgraph.checkpoint.memory import (
    InMemorySaver,
)


checkpointer = InMemorySaver()

然后:

ini 复制代码
agent = create_agent(
    model=model,
    tools=tools,
    checkpointer=checkpointer,
)

接下来每次调用:

py 复制代码
config = {
    "configurable": {
        "thread_id": "chat_001"
    }
}

再:

py 复制代码
await agent.ainvoke(
    state,
    config=config,
)

三、thread_id 如何串起多轮对话?

第一次:

py 复制代码
thread_id = chat_001

我叫猪头

最终 State:

py 复制代码
messages:
[
    HumanMessage("我叫猪头"),
    AIMessage("好的")
]

Checkpointer 保存:

复制代码
chat_001
↓
State

第二次:

ini 复制代码
thread_id = chat_001

我叫什么?

Runtime 根据:

复制代码
chat_001

恢复之前 State。

于是模型实际上看到:

scss 复制代码
HumanMessage("我叫猪头")
AIMessage("好的")
HumanMessage("我叫什么?")

就可以回答:

复制代码
你叫猪头。

如果换成:

ini 复制代码
thread_id = chat_002

则是另外一份完全独立的 State。


四、State、Context、Config 再区分一次

现在调用可能变成:

py 复制代码
await agent.ainvoke(
    state,
    config=config,
    context=context,
)

三个概念:

ini 复制代码
state
=
Agent 运行数据

context
=
业务运行环境

config
=
LangGraph Runtime 配置

例如:

py 复制代码
state = {
    "messages": [...]
}
py 复制代码
context = AgentContext(
    user_id="001",
    role="admin",
)
py 复制代码
config = {
    "configurable": {
        "thread_id": "chat_001"
    }
}

特别要注意:

复制代码
role
permissions
user_id

这类鉴权信息,不应该依赖历史 Checkpoint。

因为用户今天是:

复制代码
admin

明天权限可能已经被撤销。

所以每次 HTTP 请求都应该重新:

复制代码
验证 JWT
↓
创建 Context

而不是从旧 State 恢复。


五、Checkpointer 不是 Long-Term Memory

这是一个很容易混淆的地方。

复制代码
Checkpointer

更接近:

同一 Thread 内的短期记忆。

例如:

复制代码
thread_001

保存:

复制代码
聊天历史
任务步骤
当前 topic

但如果明天创建:

复制代码
thread_999

还希望 Agent 知道:

复制代码
用户喜欢 React
用户偏好中文

那属于:

kotlin 复制代码
Long-term Store

不是 Checkpointer。

可以简单记:

ini 复制代码
Checkpointer
=
Conversation Memory

Store
=
User Memory

六、为什么 InMemorySaver 不能上生产?

因为:

复制代码
服务重启
↓
内存消失

而且多个 Worker:

css 复制代码
Worker A
Worker B

内存互不共享。

用户第一次请求可能到:

css 复制代码
Worker A

第二次请求到:

css 复制代码
Worker B

历史就没了。

生产环境通常应该换:

复制代码
PostgreSQL
Redis

等共享持久化方案。


七、下一步:Streaming

普通接口:

复制代码
用户请求
↓
等待 8 秒
↓
答案突然出现

对于 Agent 来说体验很差。

因为 Agent 中间可能正在:

复制代码
搜索
查询数据库
调用天气
计算
分析

更好的体验应该是:

erlang 复制代码
正在查询天气...
↓
天气查询完成
↓
正在计算...
↓
最终答案逐字输出

新版 Agent 可以:

scss 复制代码
agent.astream()

进行流式处理。


八、三个非常重要的 Stream Mode

最常用的是:

vbnet 复制代码
messages
custom
updates

messages

用于:

复制代码
LLM Token / Message Chunk

例如:

arduino 复制代码
"上"
"海"
"今"
"天"

custom

用于:

复制代码
Tool 或 Node 主动发送自定义事件

例如:

复制代码
正在查询上海天气

updates

用于:

复制代码
Graph Node 执行后的 State 更新

更适合:

rust 复制代码
Agent Debug
流程 UI
Observability

九、Tool 如何主动给前端发状态?

使用:

复制代码
runtime.stream_writer

例如:

py 复制代码
@tool
def get_weather(
    city: str,
    runtime: ToolRuntime,
):
    runtime.stream_writer.write(
        {
            "type": "tool_status",
            "tool": "get_weather",
            "status": "start",
            "message":
                f"正在查询 {city} 天气",
        }
    )

    result = query_weather(city)

    runtime.stream_writer.write(
        {
            "type": "tool_status",
            "tool": "get_weather",
            "status": "success",
            "message": "天气查询完成",
        }
    )

    return result

这里 Tool 有两条输出线。

kotlin 复制代码
return result

走:

复制代码
Tool
↓
ToolMessage
↓
LLM

而:

lua 复制代码
runtime.stream_writer.write(...)

走:

vbnet 复制代码
Tool
↓
custom stream
↓
FastAPI
↓
前端

这两个目的完全不同。


十、FastAPI SSE

我们可以创建:

bash 复制代码
POST /api/chat/stream

使用:

复制代码
EventSourceResponse
ServerSentEvent

需要注意新版 FastAPI SSE 的写法。

路由本身应该直接:

arduino 复制代码
yield

例如:

py 复制代码
@router.post(
    "/stream",
    response_class=EventSourceResponse,
)
async def chat_stream(
    request: ChatRequest,
):

    async for mode, chunk in agent.astream(
        {
            "messages": [
                {
                    "role": "user",
                    "content": request.message,
                }
            ]
        },
        stream_mode=[
            "messages",
            "custom",
        ],
    ):

        if mode == "custom":

            yield ServerSentEvent(
                event="status",
                data=chunk,
            )

        elif mode == "messages":

            message_chunk, metadata = chunk

            content = message_chunk.content

            if (
                isinstance(content, str)
                and content
            ):
                yield ServerSentEvent(
                    event="token",
                    data={
                        "content": content
                    },
                )

    yield ServerSentEvent(
        event="done",
        data={}
    )

不要再:

py 复制代码
async def event_generator():
    ...

return EventSourceResponse(
    event_generator()
)

否则在新版 SSE 路由模式下可能出现:

csharp 复制代码
'coroutine' object is not iterable

十一、为什么不用 json.dumps()

新版:

py 复制代码
ServerSentEvent(
    data={
        "content": "hello"
    }
)

可以直接传 dict。

框架会负责序列化。

如果自己:

scss 复制代码
json.dumps(...)

再传进去,可能造成:

typescript 复制代码
JSON string
↓
再次 JSON encode

最终前端收到:

arduino 复制代码
"{"content":"hello"}"

需要多 parse 一次。

所以:

ini 复制代码
data={...}

即可。


十二、前端为什么不能直接用 EventSource?

因为原生:

scss 复制代码
new EventSource(...)

主要用于:

sql 复制代码
GET

但聊天接口一般需要:

diff 复制代码
POST
+
Body
+
Authorization

例如:

json 复制代码
{
  "message": "...",
  "thread_id": "..."
}

所以更常用:

diff 复制代码
fetch
+
ReadableStream

例如:

ts 复制代码
const response = await fetch(
    "/api/chat/stream",
    {
        method: "POST",
        body: JSON.stringify({
            message,
            thread_id,
        }),
    }
)

然后:

ts 复制代码
const reader =
    response.body!.getReader()

const decoder =
    new TextDecoder()

持续:

ts 复制代码
while (true) {
    const {
        value,
        done
    } = await reader.read()

    if (done) break

    const text =
        decoder.decode(
            value,
            {
                stream: true
            }
        )

    console.log(text)
}

十三、为什么不能把一次 reader.read() 当成一个 SSE Event?

因为:

复制代码
HTTP/TCP Chunk

没有你的业务边界概念。

服务端:

css 复制代码
event: token
data: {"content":"LangChain"}

客户端第一次可能收到:

makefile 复制代码
event: token
data: {"cont

第二次:

css 复制代码
ent":"LangChain"}

所以前端必须有:

arduino 复制代码
buffer
+
SSE Parser

而不能简单假设:

scss 复制代码
一个 read()
=
一个完整 Event

这和 TCP 拆包、粘包的思想非常像。


十四、Structured Output

Streaming 解决的是:

复制代码
用户体验

但另一个问题还存在:

css 复制代码
result["messages"][-1].content

仍然是一段自由文本。

前端如果需要:

复制代码
answer
category
confidence

就需要结构化输出。

例如:

ts 复制代码
from typing import Literal

from pydantic import BaseModel, Field


class AgentResponse(BaseModel):
    answer: str

    category: Literal[
        "general",
        "weather",
        "calculation",
        "account",
        "order",
    ]

    confidence: float = Field(
        ge=0,
        le=1,
    )

然后:

py 复制代码
agent = create_agent(
    model=model,
    tools=tools,

    response_format=
        AgentResponse,
)

最终:

css 复制代码
result[    "structured_response"]

得到:

ini 复制代码
AgentResponse(
    answer="上海今天多云",
    category="weather",
    confidence=0.96,
)

十五、Tool Schema 和 Structured Output 不一样

Tool:

kotlin 复制代码
class CalculatorInput(BaseModel):

解决的是:

复制代码
LLM
↓
Tool

参数结构。

例如:

css 复制代码
{
  "a": 123,
  "b": 456,
  "operator": "multiply"
}

而:

kotlin 复制代码
class AgentResponse(BaseModel):

解决的是:

复制代码
Agent
↓
应用程序

输出结构。

例如:

py 复制代码
{
  "answer": "...",
  "category": "weather",
  "confidence": 0.95
}

所以:

ini 复制代码
Tool Schema
=
工具输入契约

Structured Response
=
Agent 输出契约

十六、ProviderStrategy 和 ToolStrategy

新版 LangChain 可以通过 Provider 原生能力:

复制代码
ProviderStrategy

生成结构化输出。

也可以通过:

复制代码
ToolStrategy

把结构化输出模拟成一次特殊的 Tool Calling。

大多数时候直接:

ini 复制代码
response_format=AgentResponse

即可。

让 LangChain 根据模型能力自动选择。

这样以后从:

复制代码
OpenAI

切换到:

复制代码
其他 Provider

业务层不用改太多。


十七、Structured Output 只保证格式,不保证事实

这个非常重要。

例如模型输出:

json 复制代码
{
  "confidence": 0.99
}

Schema 可以保证:

复制代码
confidence 是数字
并且在 0~1

但不能保证:

shell 复制代码
99% 真的是统计意义上的正确率

也就是说:

ini 复制代码
Structured Output
=
Shape Reliability

不等于:

复制代码
Truth Reliability

所以像:

复制代码
used_tools
真实执行耗时
Token 数量
权限结果
计费

这类信息应该由系统 Runtime / Middleware 提供,而不是让模型自己生成。


十八、目前整个 Agent 架构

现在我们的项目已经形成:

复制代码
React
  │
  ▼
FastAPI
  │
  ├──────── JWT / Context
  │
  ├──────── thread_id
  │
  ▼
create_agent
  │
  ├──────── Model
  │
  ├──────── Tools
  │
  ├──────── Middleware
  │
  ├──────── State
  │
  ├──────── Checkpointer
  │
  ├──────── Streaming
  │
  └──────── Structured Output

Tool 又可以通过:

perl 复制代码
ToolRuntime
├── context
├── state
├── tool_call_id
└── stream_writer

和整个 Runtime 交互。

这已经不再只是:

复制代码
调用一下 LLM API

而是一个真正:

复制代码
有状态
有工具
有权限
有生命周期
有流式事件
有结构化契约

的 Agent Backend。


十九、学完这三篇之后,你应该具备的知识结构

第一层:

vbnet 复制代码
create_agent
Tool Calling
ToolMessage
Agent Loop

第二层:

复制代码
State
Reducer
Context
ToolRuntime
Command
Middleware

第三层:

复制代码
Checkpointer
thread_id
Streaming
SSE
Structured Output

如果你把这三层吃透,新版 LangChain Agent 的核心主干其实已经掌握了大半。

下一阶段再继续学习:

sql 复制代码
Dynamic Tools
Dynamic Model
官方 Middleware
Long-term Store
Context Engineering
Human-in-the-loop
LangSmith
MCP
Agentic RAG
Multi-Agent
Deep Agents

会顺很多,因为后面的能力基本都是在这套 Runtime 之上继续扩展。

相关推荐
染指11103 小时前
110.Agent-LangChain核心组件-Messages消息和提示词工程
人工智能·microsoft·langchain·agents
minhuan3 小时前
搭建本地人脸识别系统:解析FastAPI+InsightFace+FAISS全栈实现原理与优化方案26.6
fastapi·faiss·insightface·大模型应用·人脸识别模型·本地化人脸识别系统
tryCbest4 小时前
FastAPI中passlib包的作用
python·fastapi·passlib
zzzll11116 小时前
LangChain 1.3 新特性详解与实战指南
java·数据库·langchain
meilindehuzi_a6 小时前
LangChain.js + Milvus 向量长期记忆实战:对话写入、语义检索与 RAG 增强
javascript·langchain·milvus
ToTensor21 小时前
DataGen——合成数据生成器:把一句任务描述变成可校验的训练数据
langchain·agent
青 春 记 忆1 天前
零基础入门python66:FastAPI AI标题、摘要和标签
python·fastapi·后端开发
青 春 记 忆1 天前
零基础入门python65:FastAPI 安全调用大模型API
python·fastapi·后端开发
逆风飞翔的小叔1 天前
【Python 基础】FastAPI ORM 操作MySql 实战使用详解
fastapi·fastapi orm·fastapi orm详解·fastapi orm使用·fastapi orm操作