完整交付——LocalTrend 系统集成与生产化 — LangGraph 实战——构建跨平台爆款图文 Agent 第8篇

第8章:完整交付——LocalTrend 系统集成与生产化

本章目标

读完本章你会:

  • 能将前 7 章的碎片化组件集成为一个完整可运行的 LocalTrend 系统
  • 能用 .stream() 实现流式输出,让用户实时看到 Agent 的思考和行动
  • 能为 Agent 系统添加结构化错误处理,优雅应对 API 故障和网络异常
  • 能用 FastAPI + SSE 把 LocalTrend 暴露为 Web 服务
  • 能对着生产部署清单逐项确认,把一个开发版 Agent 推向生产

知识讲解

从一个生活例子开始

你开了一家新餐厅。过去几个月,你分别搞定了:

  • 厨房设备(第 1 章:图的基础设施)
  • 菜单设计(第 2 章:ReAct 模式)
  • POS 记账系统(第 3 章:持久化)
  • 服务员培训(第 4 章:人工审核)
  • 多窗口同时出餐(第 5 章:并行执行)
  • 厨师团队分工(第 6 章:多 Agent)
  • 食材供应链(第 7 章:Agentic RAG)

今天要开门营业。这意味着:

  1. 集成测试:所有系统串起来跑一遍——从客人点单到结账离店
  2. 监控面板:厨房能看到前厅订单实时流入(流式输出)
  3. 应急预案:某个灶台坏了不影响其他菜(错误处理)
  4. 线上外卖:不只服务堂食,还接外卖平台订单(Web 服务)
  5. 运营手册:卫生标准、排班表、供应商联系方式(部署清单)

这就是本章要做的——不是教新厨具,而是把厨房开成一家餐厅

架构总览

学完本章后,你的 LocalTrend 系统架构如下:

┌─────────────────────────────────────────────────┐
│                   FastAPI Server                 │
│  GET  /localtrend/stream?sse=true  ← SSE 流式    │
│  POST /localtrend/invoke           ← 同步调用    │
│  GET  /localtrend/health           ← 健康检查    │
└──────────────┬──────────────────────────────────┘
               │
┌──────────────▼──────────────────────────────────┐
│              LocalTrend Supervisor               │
│  ┌──────────┐ ┌──────────┐ ┌──────┐ ┌────────┐ │
│  │Researcher│ │ Analyst  │ │Writer│ │Publisher│ │
│  │(RAG子图) │ │ (子图)   │ │(子图)│ │(子图)  │ │
│  └──────────┘ └──────────┘ └──────┘ └────────┘ │
│         ↓            ↓          ↓         ↓     │
│  ┌──────────────────────────────────────────┐   │
│  │        SqliteSaver (持久化)               │   │
│  └──────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘

代码实战

环境准备

pip install -U fastapi uvicorn sse-starlette

Step 1:完整的 LocalTrend 集成模块

把第 6-7 章的组件整合为一个干净的可导入模块。新建 localtrend_engine.py

"""
localtrend_engine.py — LocalTrend 核心引擎
集成前 7 章全部能力的统一入口
"""
import os
import sqlite3
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command

from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI


# ============================================================
# 全局配置
# ============================================================
class Config:
    LLM_MODEL = "deepseek-chat"
    LLM_BASE_URL = "https://api.deepseek.com"
    LLM_API_KEY = os.getenv("DEEPSEEK_API_KEY", "your-api-key-here")
    DB_PATH = os.getenv("LOCALTREND_DB", "localtrend_production.db")
    MAX_RETRIES = 3
    DEFAULT_PLATFORMS = ["微信公众号", "小红书", "微博", "B站", "知乎"]


def get_llm(temperature: float = 0.7):
    return ChatOpenAI(
        model=Config.LLM_MODEL,
        api_key=Config.LLM_API_KEY,
        base_url=Config.LLM_BASE_URL,
        temperature=temperature,
    )


# ============================================================
# 统一的 State Schema
# ============================================================
class LocalTrendState(TypedDict):
    messages: Annotated[list, add_messages]
    next_agent: str

    # Researcher (RAG)
    search_query: str
    all_findings: list[dict]
    research_results: str

    # Analyst
    trend_patterns: str

    # Writer
    platform: str
    generated_content: str

    # Publisher
    review_decision: str
    review_feedback: str
    published: bool

    # 错误处理
    error: str
    retry_count: int


# ============================================================
# 搜索工具
# ============================================================
@tool
def search_web(query: str) -> str:
    """搜索网页获取最新信息。参数 query: 搜索关键词"""
    tavily_key = os.getenv("TAVILY_API_KEY", "")
    if tavily_key:
        try:
            from tavily import TavilyClient
            client = TavilyClient(api_key=tavily_key)
            response = client.search(query, max_results=3)
            results = [r.get("content", "") for r in response.get("results", [])]
            return "\n\n".join(results) if results else f"关于'{query}'暂无搜索结果"
        except Exception as e:
            return f"搜索出错: {e}"
    # Fallback
    try:
        from duckduckgo_search import DDGS
        with DDGS() as ddgs:
            results = [r.get("body", "") for r in ddgs.text(query, max_results=3)]
            return "\n\n".join(results) if results else f"关于'{query}'暂无搜索结果"
    except Exception:
        return f"[模拟搜索] 关于'{query}'的趋势分析数据(DuckDuckGo 不可用,使用 fallback)。"


# ============================================================
# 子图:Researcher (RAG 版,第 7 章)
# ============================================================
def create_researcher():
    llm = get_llm(0.3)
    llm_with_tools = llm.bind_tools([search_web])

    def research(state: LocalTrendState) -> dict:
        user_msgs = [m for m in state["messages"] if isinstance(m, HumanMessage)]
        topic = user_msgs[-1].content if user_msgs else "社交媒体爆款趋势"
        response = llm_with_tools.invoke([
            HumanMessage(content=f"搜索'{topic}'的写作规律和标题设计技巧。使用搜索工具。")
        ])
        findings = []
        if hasattr(response, "tool_calls") and response.tool_calls:
            for tc in response.tool_calls:
                result = search_web.invoke(tc["args"])
                findings.append({"title": tc["args"].get("query", ""), "content": result})
        summary = "\n\n".join([f"## {f['title']}\n{f['content'][:500]}" for f in findings])
        return {
            "messages": [response],
            "research_results": summary,
            "all_findings": findings,
        }

    builder = StateGraph(LocalTrendState)
    builder.add_node("research", research)
    builder.add_edge(START, "research")
    builder.add_edge("research", END)
    return builder.compile()


# ============================================================
# 子图:Analyst (第 6 章)
# ============================================================
def create_analyst():
    llm = get_llm(0.5)

    def analyze(state: LocalTrendState) -> dict:
        research = state.get("research_results", "")
        response = llm.invoke([
            HumanMessage(content=f"提炼 3-5 条爆款内容规律:\n{research}")
        ])
        return {
            "messages": [AIMessage(content=f" {response.content[:100]}...")],
            "trend_patterns": response.content,
        }

    builder = StateGraph(LocalTrendState)
    builder.add_node("analyze", analyze)
    builder.add_edge(START, "analyze")
    builder.add_edge("analyze", END)
    return builder.compile()


# ============================================================
# 子图:Writer (第 6 章,支持驳回重写)
# ============================================================
def create_writer():
    llm = get_llm(0.8)

    def write(state: LocalTrendState) -> dict:
        patterns = state.get("trend_patterns", "")
        platform = state.get("platform", "微信公众号")
        feedback = state.get("review_feedback", "")

        prompt = f"为{platform}写一篇 200 字爆款短文。规律:\n{patterns}\n"
        if feedback:
            prompt += f"\n上一版驳回意见:{feedback}\n请据此改进。"

        response = llm.invoke([HumanMessage(content=prompt)])
        return {
            "messages": [AIMessage(content=f"✍️ {response.content[:80]}...")],
            "generated_content": response.content,
            "review_feedback": "",
        }

    builder = StateGraph(LocalTrendState)
    builder.add_node("write", write)
    builder.add_edge(START, "write")
    builder.add_edge("write", END)
    return builder.compile()


# ============================================================
# 子图:Publisher (第 4+6 章)
# ============================================================
def create_publisher():
    def review(state: LocalTrendState) -> dict:
        content = state.get("generated_content", "")
        platform = state.get("platform", "微信公众号")
        print(f"\n{'='*50}\n [{platform}] 待审核:\n{content[:200]}\n{'='*50}")

        decision = interrupt(f"审核{platform}内容: approve(批准) 或 输入修改意见")
        if decision.lower() in ("approve", "批准"):
            return {"review_decision": "approved", "published": True}
        return {"review_decision": "rejected", "review_feedback": decision}

    builder = StateGraph(LocalTrendState)
    builder.add_node("review", review)
    builder.add_edge(START, "review")
    builder.add_edge("review", END)
    return builder.compile()


# ============================================================
# 父图:Supervisor
# ============================================================
def build_localtrend(db_path: str = None):
    if db_path is None:
        db_path = Config.DB_PATH

    builder = StateGraph(LocalTrendState)
    builder.add_node("researcher", create_researcher())
    builder.add_node("analyst", create_analyst())
    builder.add_node("writer", create_writer())
    builder.add_node("publisher", create_publisher())

    supervisor_llm = get_llm(0)

    def supervisor(state: LocalTrendState) -> dict:
        r = state.get("research_results", "")
        p = state.get("trend_patterns", "")
        c = state.get("generated_content", "")
        fb = state.get("review_feedback", "")

        response = supervisor_llm.invoke([
            HumanMessage(content=f"""进度:搜索{'✅' if r else '❌'} 分析{'✅' if p else '❌'} 写作{'✅' if c else '❌'}
驳回意见: {fb if fb else '无'}
决定下一步(只回复一个词): researcher / analyst / writer / publisher / finish""")
        ])
        return {"next_agent": response.content.strip().lower()}

    def router(state: LocalTrendState) -> Literal[
        "researcher", "analyst", "writer", "publisher", "__end__"
    ]:
        return state.get("next_agent", "__end__")  # type: ignore

    builder.add_node("supervisor", supervisor)
    builder.add_edge(START, "supervisor")
    builder.add_conditional_edges("supervisor", router, {
        "researcher": "researcher", "analyst": "analyst",
        "writer": "writer", "publisher": "publisher", "__end__": END,
    })
    builder.add_edge("researcher", "supervisor")
    builder.add_edge("analyst", "supervisor")
    builder.add_edge("writer", "supervisor")
    builder.add_edge("publisher", "supervisor")

    conn = sqlite3.connect(db_path, check_same_thread=False)
    return builder.compile(checkpointer=SqliteSaver(conn))


# ============================================================
#  带错误处理的执行包装器
# ============================================================
def safe_invoke(graph, state: dict, config: dict, max_retries: int = None):
    """带重试和错误恢复的 invoke 包装器"""
    if max_retries is None:
        max_retries = Config.MAX_RETRIES

    for attempt in range(max_retries + 1):
        try:
            result = graph.invoke(state, config)
            return result
        except Exception as e:
            error_msg = str(e)
            print(f"⚠️ 执行失败 (尝试 {attempt + 1}/{max_retries + 1}): {error_msg[:100]}")

            if attempt < max_retries:
                # 重试前等待递增
                import time
                wait = (attempt + 1) * 2
                print(f"  ⏳ {wait} 秒后重试...")
                time.sleep(wait)
            else:
                # 返回错误状态而非崩溃
                state["error"] = error_msg
                state["retry_count"] = attempt + 1
                return state
    return state

Step 2:流式输出

LangGraph 的 .stream() 方法让你实时观察每一步的执行。新建 chapter08_stream_demo.py

"""
第 8 章 演示:流式输出
用 .stream() 实时观察 LocalTrend 的每一步执行
"""
from localtrend_engine import build_localtrend, LocalTrendState
from langchain_core.messages import HumanMessage
from langgraph.types import Command


def run_with_streaming():
    graph = build_localtrend("localtrend_stream_demo.db")
    config = {"configurable": {"thread_id": "stream-demo-1"}}

    initial_state = {
        "messages": [HumanMessage(content="分析 AI 领域的爆款写作规律,为小红书写一篇笔记")],
        "next_agent": "",
        "search_query": "",
        "all_findings": [],
        "research_results": "",
        "trend_patterns": "",
        "platform": "小红书",
        "generated_content": "",
        "review_decision": "",
        "review_feedback": "",
        "published": False,
        "error": "",
        "retry_count": 0,
    }

    print("=" * 60)
    print(" LocalTrend 流式执行")
    print("=" * 60)

    #  stream_mode="updates" 返回每个节点执行后的 State 变更
    step = 0
    for chunk in graph.stream(initial_state, config, stream_mode="updates"):
        step += 1
        node_name = list(chunk.keys())[0] if chunk else "unknown"
        node_output = chunk.get(node_name, {})

        # 提取有意义的输出摘要
        summary_parts = []
        for key, value in node_output.items():
            if isinstance(value, str) and len(value) > 0:
                summary_parts.append(f"{key}: {str(value)[:60]}...")
            elif isinstance(value, list) and len(value) > 0:
                summary_parts.append(f"{key}: [{len(value)} 条]")
            elif isinstance(value, bool):
                summary_parts.append(f"{key}: {'✅' if value else '❌'}")

        summary = " | ".join(summary_parts[:3]) if summary_parts else "(内部处理)"
        print(f"\n Step {step}: [{node_name}] {summary}")

    print(f"\n{'='*60}")
    print("⏸️ 图在 Publisher 中断,等待审核...")
    print("   (在真实 UI 中,这里会展示内容给用户)")

    # 模拟审核
    print("\n 审核通过!")
    for chunk in graph.stream(
        Command(resume="approve"),
        config,
        stream_mode="updates"
    ):
        step += 1
        node_name = list(chunk.keys())[0]
        print(f" Step {step}: [{node_name}]")

    print("\n✅ LocalTrend 执行完成!")


if __name__ == "__main__":
    run_with_streaming()

stream_mode 选项说明:

mode 返回什么 适用场景
"updates" 每个节点执行后的 State 变更 dict 进度展示、调试
"values" 每个 super-step 后的完整 State 需要完整上下文
"messages" 仅消息流(需 message 类型 State) 聊天界面逐字展示
"debug" 最详细的调试信息 排查问题时使用

Step 3:Web 服务化——FastAPI + SSE

新建 chapter08_server.py

"""
第 8 章 演示:LocalTrend Web 服务
FastAPI + SSE 流式输出,可通过浏览器或 curl 调用
"""
import json
import uuid
import asyncio
from typing import AsyncGenerator

from fastapi import FastAPI, Query
from fastapi.responses import StreamingResponse
from sse_starlette.sse import EventSourceResponse
from pydantic import BaseModel

from localtrend_engine import build_localtrend
from langchain_core.messages import HumanMessage
from langgraph.types import Command

app = FastAPI(title="LocalTrend API", version="1.0.0")

# 全局图实例(生产环境用 PostgresSaver + 连接池)
graph = build_localtrend()


# ============================================================
# GET / — 根路径欢迎页
# ============================================================
@app.get("/")
async def root():
    return {
        "service": "LocalTrend API",
        "version": "1.0.0",
        "endpoints": {
            "analyze": "POST /localtrend/analyze",
            "stream": "GET /localtrend/stream?topic=AI&platform=公众号",
            "content": "GET /localtrend/content/{thread_id}",
            "review": "POST /localtrend/review",
            "health": "GET /localtrend/health",
        },
        "docs": "/docs",
    }


# ============================================================
# 请求/响应模型
# ============================================================
class TrendRequest(BaseModel):
    topic: str = "AI 爆款趋势"
    platform: str = "微信公众号"
    thread_id: str = ""


class ReviewRequest(BaseModel):
    thread_id: str
    decision: str  # "approve" 或 修改意见


# ============================================================
# POST /localtrend/analyze — 同步调用(不含审核中断)
# ============================================================
@app.post("/localtrend/analyze")
async def analyze_trends(req: TrendRequest):
    """同步分析趋势(不包含人工审核——用于快速测试)"""
    thread_id = req.thread_id or str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    result = graph.invoke(
        {
            "messages": [HumanMessage(content=req.topic)],
            "next_agent": "",
            "search_query": "",
            "all_findings": [],
            "research_results": "",
            "trend_patterns": "",
            "platform": req.platform,
            "generated_content": "",
            "review_decision": "",
            "review_feedback": "",
            "published": False,
            "error": "",
            "retry_count": 0,
        },
        config
    )
    return {
        "thread_id": thread_id,
        "research": result.get("research_results", "")[:500],
        "patterns": result.get("trend_patterns", "")[:500],
        "generated": result.get("generated_content", "")[:500],
    }


# ============================================================
# GET /localtrend/stream — SSE 流式输出
# ============================================================
@app.get("/localtrend/stream")
async def stream_analyze(
    topic: str = Query(default="AI 爆款趋势"),
    platform: str = Query(default="微信公众号"),
):
    """SSE 流式——实时推送每一步的执行状态"""
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    async def event_stream() -> AsyncGenerator[dict, None]:
        initial_state = {
            "messages": [HumanMessage(content=topic)],
            "next_agent": "",
            "search_query": "",
            "all_findings": [],
            "research_results": "",
            "trend_patterns": "",
            "platform": platform,
            "generated_content": "",
            "review_decision": "",
            "review_feedback": "",
            "published": False,
            "error": "",
            "retry_count": 0,
        }

        try:
            for chunk in graph.stream(initial_state, config, stream_mode="updates"):
                node_name = list(chunk.keys())[0]
                yield {
                    "event": "node_update",
                    "data": json.dumps({
                        "thread_id": thread_id,
                        "node": node_name,
                        "summary": _summarize_chunk(chunk),
                    }, ensure_ascii=False),
                }
                await asyncio.sleep(0)  # 让出控制权给事件循环

            yield {
                "event": "complete",
                "data": json.dumps({"thread_id": thread_id, "status": "waiting_review"})
            }
        except Exception as e:
            yield {
                "event": "error",
                "data": json.dumps({"error": str(e)})
            }

    return EventSourceResponse(event_stream())


def _summarize_chunk(chunk: dict) -> str:
    """提取 chunk 的摘要"""
    node_name = list(chunk.keys())[0]
    updates = chunk[node_name]
    parts = []
    for k, v in updates.items():
        if isinstance(v, str) and len(v) > 0:
            parts.append(f"{k}: {v[:80]}...")
        elif isinstance(v, list):
            parts.append(f"{k}: [{len(v)} items]")
    return f"[{node_name}] " + " | ".join(parts[:2]) if parts else f"[{node_name}] 执行中"


# ============================================================
# GET /localtrend/content/{thread_id} — 查询生成的内容
# ============================================================
@app.get("/localtrend/content/{thread_id}")
async def get_content(thread_id: str):
    """查询指定会话的当前状态——包括生成的文章、分析结果等"""
    config = {"configurable": {"thread_id": thread_id}}

    try:
        state = graph.get_state(config)
        if state is None or state.values is None:
            return {"thread_id": thread_id, "status": "not_found", "message": "该会话不存在或已过期"}

        values = state.values
        return {
            "thread_id": thread_id,
            "status": "waiting_review" if not values.get("published") else "published",
            "platform": values.get("platform", ""),
            "research_results": values.get("research_results", "")[:500],
            "trend_patterns": values.get("trend_patterns", "")[:500],
            "generated_content": values.get("generated_content", ""),
            "review_feedback": values.get("review_feedback", ""),
            "published": values.get("published", False),
            "error": values.get("error", ""),
        }
    except Exception as e:
        return {"thread_id": thread_id, "error": str(e)}


# ============================================================
# POST /localtrend/review — 提交审核决定
# ============================================================
@app.post("/localtrend/review")
async def submit_review(req: ReviewRequest):
    """提交人工审核决定,恢复暂停的图"""
    config = {"configurable": {"thread_id": req.thread_id}}

    try:
        result = graph.invoke(Command(resume=req.decision), config)
        return {
            "thread_id": req.thread_id,
            "published": result.get("published", False),
            "content": result.get("generated_content", "")[:500],
        }
    except Exception as e:
        return {"error": str(e), "thread_id": req.thread_id}


# ============================================================
# GET /localtrend/health — 健康检查
# ============================================================
@app.get("/localtrend/health")
async def health():
    return {"status": "ok", "service": "LocalTrend"}


# ============================================================
# 启动
# ============================================================
if __name__ == "__main__":
    import uvicorn
    print("=" * 60)
    print(" LocalTrend API 服务启动")
    print("=" * 60)
    print(" 端点:")
    print("  POST /localtrend/analyze          — 同步趋势分析")
    print("  GET  /localtrend/stream           — SSE 流式分析")
    print("  GET  /localtrend/content/{id}     — 查询生成的内容")
    print("  POST /localtrend/review           — 提交审核决定")
    print("  GET  /localtrend/health           — 健康检查")
    print("=" * 60)
    uvicorn.run(app, host="0.0.0.0", port=8000)

启动后,打开另一个终端测试:

# Windows PowerShell
# 终端 A:启动服务
python chapter08_server.py

# 终端 B:测试接口

# 健康检查
curl http://localhost:8000/localtrend/health

# 同步分析(不含审核)
curl -X POST http://localhost:8000/localtrend/analyze \
  -H "Content-Type: application/json" \
  -d '{"topic": "AI 写作技巧", "platform": "小红书"}'

# 流式分析(在浏览器中打开,观察实时推送)
# http://localhost:8000/localtrend/stream?topic=AI爆款&platform=微博
# 推送结束后,记录返回的 thread_id

# 查看生成的文章(用上面记录的 thread_id 替换 {id})
curl http://localhost:8000/localtrend/content/{id}

# 审核通过,发布文章
curl -X POST http://localhost:8000/localtrend/review \
  -H "Content-Type: application/json" \
  -d '{"thread_id": "{id}", "decision": "approve"}'

# 驳回并给出修改意见
curl -X POST http://localhost:8000/localtrend/review \
  -H "Content-Type: application/json" \
  -d '{"thread_id": "{id}", "decision": "标题不够吸引人,加 emoji"}'

生产部署清单

将 LocalTrend 从开发环境推向生产,逐项确认:

持久化

项目 开发 生产
Checkpointer SqliteSaver(本地文件) PostgresSaver(数据库连接池)
数据库连接 sqlite3.connect(db_path) asyncpg + 连接池(min_size=5, max_size=20
会话隔离 thread_id thread_id(不变——但加上用户认证层)

安全

项目 检查点
API Key 不在代码中硬编码——全部走环境变量或 Secret Manager
认证 FastAPI 加 Depends(get_current_user) 依赖注入(JWT 或 API Key)
速率限制 slowapi 限制每个用户的每分钟请求数
输入校验 Pydantic 模型 + max_length 限制 + 敏感词过滤

可靠性

项目 方案
LLM 调用失败 重试 3 次 + 指数退避(已在 safe_invoke 中实现)
Web 搜索失败 fallback 到 DuckDuckGo → 模拟数据 → 优雅降级
数据库故障 连接池自动重连 + 健康检查端点给负载均衡用
interrupt 超时 定时任务扫描超过 24h 未审核的 thread,自动驳回或升级告警

可观测性

项目 工具
链路追踪 LangSmith(设置 LANGCHAIN_TRACING_V2=true
日志 structlog(结构化日志,方便检索)
指标 Prometheus + prometheus-fastapi-instrumentator
告警 PagerDuty / 企业微信 Webhook(错误率 > 5% 触发)

部署

项目 推荐方案
容器化 Docker + docker-compose(本地)/ Kubernetes(集群)
CI/CD GitHub Actions:pytestruff checkdocker build → deploy
反向代理 Nginx(处理 SSL 终结 + 静态文件 + 限流)
进程管理 gunicorn + uvicorn.workers.UvicornWorker(4-8 workers)

最低可上线配置(docker-compose.yml)

# docker-compose.yml
version: "3.9"
services:
  localtrend:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
      - TAVILY_API_KEY=${TAVILY_API_KEY}
      - LOCALTREND_DB=postgresql://user:pass@db:5432/localtrend
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: localtrend
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

本章小结

  1. 系统集成——把前 7 章的子图、工具、checkpointer 组装为一个可导入的 localtrend_engine 模块。
  2. 流式输出——.stream(stream_mode="updates") 实时推送每个节点的执行状态,给用户"看得见的 AI 在工作"。
  3. 错误处理——safe_invoke 包装器提供重试 + 降级 + 错误状态返回,而非崩溃。
  4. Web 服务化——FastAPI + SSE 把 LangGraph Agent 变成一个标准 HTTP 服务,前端可以用任何框架对接。
  5. 生产清单——持久化、安全、可靠性、可观测性、部署,五项逐项确认。
  6. LocalTrend 是你简历上最亮的项目——它涵盖了 Agent 设计、多 Agent 协作、RAG、流式输出、Web 服务化,面试官会追着问。

关键术语

术语 释义
stream() LangGraph 的流式执行方法,支持 updates/values/messages/debug 四种模式
SSE (Server-Sent Events) 服务器向客户端单向推送事件的 HTTP 协议,适合 Agent 实时状态展示
safe_invoke 带重试和错误恢复的 invoke 包装器——Agent 系统的"断路器"
EventSourceResponse FastAPI/Starlette 中实现 SSE 的响应类,来自 sse-starlette
生产化 将开发版 Agent 升级为具备持久化、安全、可观测、可部署能力的生产系统

结语

八章,我们一起走完了从"什么是图"到"一个完整的、可部署的多 Agent 系统"的完整旅程。

回顾一下你构建的 LocalTrend:

图的基础(第1章)→ ReAct 思考循环(第2章)→ 持久化记忆(第3章)
→ 人工审核网关(第4章)→ 五平台并行探索(第5章)
→ 四 Agent 团队协作(第6章)→ Agentic RAG 智能检索(第7章)
→ 流式 Web 服务 + 生产部署(第8章)

每一个概念都不是孤立的——第 1 章的 State reducer 支撑了第 5 章的并行合并,第 3 章的 Checkpointer 是第 4 章 interrupt 的前置依赖,第 2 章的 ReAct 模式是第 7 章 Agentic RAG 循环的思想来源。

延伸学习路径

本教程覆盖了 LangGraph 核心能力的 80%。以下是你接下来可以深入的方向:

方向 关键词 适合场景
MCP 协议 Model Context Protocol,动态工具注册 让 Agent 接入任意第三方工具
LangGraph Platform 云部署、Cron Jobs、A/B 测试 生产级托管和运维
多模态 Agent 图片/视频分析、DALL-E 生成 你的爆款图文 Agent 需要封面图
评估与测试 LangSmith Evaluation、回归测试 确保 Agent 改版后质量不降
RAG 进阶 向量数据库(ChromaDB/Qdrant)、HyDE、Re-ranking 你的 Researcher 需要更精准的检索
自主 Agent 循环 长时任务执行、自我目标拆解 Agent 自己决定"该做什么"而不只是"被告诉做什么"

面试怎么说

如果有人问你"你做过的最复杂的项目是什么"——

"我用 LangGraph 构建了一个跨平台爆款内容分析系统 LocalTrend。它是一个多 Agent 协作系统,包含四个专业子 Agent——Researcher 使用 Agentic RAG 从五个社交媒体平台收集趋势数据,Analyst 提炼跨平台爆款规律,Writer 生成平台定制化内容,Publisher 负责人工审核和发布。整个系统支持流式输出、会话持久化、多轮人机协作,并用 FastAPI + SSE 暴露为 Web 服务。"

这 5 句话涵盖了:多 Agent 架构、RAG、流式输出、持久化、Human-in-the-Loop、Web 服务化——每一个都是面试官眼中的加分项。


感谢你完成了这趟学习旅程。祝求职顺利,代码如诗。

posted @ 2026-06-16 11:40  Yobeeo  阅读(2)  评论(0)    收藏  举报