完整交付——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)
今天要开门营业。这意味着:
- 集成测试:所有系统串起来跑一遍——从客人点单到结账离店
- 监控面板:厨房能看到前厅订单实时流入(流式输出)
- 应急预案:某个灶台坏了不影响其他菜(错误处理)
- 线上外卖:不只服务堂食,还接外卖平台订单(Web 服务)
- 运营手册:卫生标准、排班表、供应商联系方式(部署清单)
这就是本章要做的——不是教新厨具,而是把厨房开成一家餐厅。
架构总览
学完本章后,你的 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:pytest → ruff check → docker 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:
本章小结
- 系统集成——把前 7 章的子图、工具、checkpointer 组装为一个可导入的
localtrend_engine模块。 - 流式输出——
.stream(stream_mode="updates")实时推送每个节点的执行状态,给用户"看得见的 AI 在工作"。 - 错误处理——
safe_invoke包装器提供重试 + 降级 + 错误状态返回,而非崩溃。 - Web 服务化——FastAPI + SSE 把 LangGraph Agent 变成一个标准 HTTP 服务,前端可以用任何框架对接。
- 生产清单——持久化、安全、可靠性、可观测性、部署,五项逐项确认。
- 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 服务化——每一个都是面试官眼中的加分项。
感谢你完成了这趟学习旅程。祝求职顺利,代码如诗。

浙公网安备 33010602011771号