第五篇:从本地脚本到 HTTP 服务
前四篇都在本机跑脚本。最后一篇处理两件事:出问题时怎么看见内部发生了什么,以及怎么把图变成一个别人能调用的服务。涉及 test8.py 和 test21/test21.py。
先能看见,再谈优化
test8.py 接的是 LangSmith:
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "..."
os.environ["LANGSMITH_PROJECT"] = "MultiAgent-DeepSeek"
开完这三个环境变量,图里每个节点、每次模型调用、每次工具调用都会上报到 smith.langchain.com。页面上能看到调用树、耗时、输入输出,还能把测试集固定下来做回归。
写 Agent 的时候,print 只能看到自己埋的点。追踪能看到模型每一轮收到什么、回了什么,调提示词靠它省时间。
同时提醒一句:test8.py 里的 key 是明文写在代码里的,这行得挪到 .env,密钥最好也去平台重置一次。这个脚本是我最早写的,当时没在意。
图得一直活着
test21 目录里三个文件,.env、langgraph.json、test21.py。langgraph.json 告诉平台去哪找图:
{
"dependencies": ["."],
"graphs": { "piano_agent": "./test21.py:graph" },
"env": "./.env"
}
第一次我直接把 test18.py 的 MySQL 代码抄过来,服务起不来。原因是 from_conn_string() 在 1.2.x 里是上下文管理器,with 块结束连接就关了,而平台要求图在模块级常驻。
改成手动建连接,连接一直开着:
conn = pymysql.connect(**conn_params, autocommit=True)
checkpointer = PyMySQLSaver(conn)
graph = builder.compile(checkpointer=checkpointer, name="piano_agent_graph")
compile() 里的 name 是给平台看的图名。本地用 langgraph dev 起服务,之后就能通过 HTTP 调这个图,thread_id、断点、历史这些能力和脚本里一样。
五个已经确认的坑
写到这里,把能复现的问题列清楚。
test19.py 漏了 from langgraph.graph import StateGraph, START, END,运行直接 NameError,补一行就好。
test20.py 依赖 PostgresSaver,环境里没装 langgraph-checkpoint-postgres,也没起 PG 服务。换成 SqliteSaver 或 PyMySQLSaver 就能验证声明式中断。
test8.py 的密钥明文,要挪走并重置。
节点函数返回完整 state 会覆盖别的节点写进去的字段。只返回增量,合并交给 reducer。
没有挂 checkpointer 时,interrupt 和时间回溯都不工作。报错信息不明显,容易误判成 API 用法写错。
完整示例
部署需要一个目录、三份文件。第一份是图本身,注意连接放在模块级,图名写进 compile():
# agent_graph.py
import sqlite3
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
class State(TypedDict):
prompt: str
content: str
def generate(state: State):
return {"content": f"编曲方案:{state['prompt']},速度 80 BPM,C 大调"}
def save(state: State):
return {"content": state["content"] + "(已存档)"}
builder = StateGraph(State)
builder.add_node("generate", generate)
builder.add_node("save", save)
builder.add_edge(START, "generate")
builder.add_edge("generate", "save")
builder.add_edge("save", END)
conn = sqlite3.connect("platform_checkpoints.sqlite", check_same_thread=False)
checkpointer = SqliteSaver(conn)
checkpointer.setup()
graph = builder.compile(
checkpointer=checkpointer,
name="piano_agent_graph",
)
第二份是 langgraph.json,写给平台看:
{
"dependencies": ["."],
"graphs": { "piano_agent": "./agent_graph.py:graph" },
"env": "./.env"
}
第三份是调用脚本,用 HTTP 打这个服务:
# call_api.py
import requests
BASE = "http://127.0.0.1:2024"
response = requests.post(
f"{BASE}/runs/wait",
json={
"assistant_id": "piano_agent",
"input": {"prompt": "写一段睡前钢琴曲的方案", "content": ""},
"config": {"configurable": {"thread_id": "api-1"}},
},
)
print(response.json())
运行顺序:
langgraph dev
python call_api.py
服务起来之后,call_api.py 会打印返回的 state,content 字段里能看到两段拼接后的文本。把 thread_id 换成另一个值,服务端会当成新会话,历史不会串。
要接 MySQL,把 agent_graph.py 里的连接换成 PyMySQLSaver,其余不用动。如果服务起不来,先检查有没有把连接写进 with 块里。
系列收尾
五篇写完,22 个脚本其实只讲了一件事:把流程从代码里拆出来,变成一张能暂停、能回放的图。模型是零件,图是骨架,checkpointer 让骨架活起来。
LangGraph 的学习曲线陡的地方在概念,不在 API。StateGraph、add_conditional_edges、compile 这几个调用看一遍就会,真正花时间的是想清楚状态该有哪些字段、什么时候该停、记忆放哪一层。
下一步我打算把 test21 里的 piano_agent 补成完整项目:supervisor 调度、工具调用、MySQL 存档、审核中断、LangSmith 追踪,一条链路跑通。散脚本学得快,串成项目才记得住。
系列目录:
-
第一篇:一张状态图是怎么跑起来的
-
第二篇:Agent 是怎么转起来的
-
第三篇:让图记住,也让它停下来
-
第四篇:多个 Agent 怎么分工
-
第五篇:从本地脚本到 HTTP 服务