最近在我们系统里想给管理端和企业端都塞一个 AI Agent,聊着聊着发现 LangChain 的 agent 这套东西比想象中好上手。底层模型用的阿里云百炼的通义千问 qwen3.7-plus,走的是 OpenAI 兼容接口,所以 client 写法跟调 OpenAI 几乎没差。

这篇不是教程,就是把我拆出来的三个 case 文件捋一遍:从最底层的图,到带工具带护栏的 agent,凑一条能跑通的学习路径。三个文件都在 boos-api/llm/ 下:case17case18case19


共用的一段 LLM 配置

三个案例吃的都是同一段 client,先贴出来省得后面重复:

from langchain_openai import ChatOpenAI
import os

llm = ChatOpenAI(
    model="qwen-plus",
    temperature=0.75,
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https:de/v1"
)

base_url 换成百炼给的 compatible-mode 地址,modelqwen-plus,剩下照搬 OpenAI 的写法就行。temperature 我一般给 0.75,让回答别太死板。跑之前记得 export 一下 DASHSCOPE_API_KEY


case19 —— 先把图的骨架看明白

LangGraph 不管多复杂的 agent,剥到底就是三样东西:状态(State)、节点(Node)、边(Edge)。case19 是最干净的一版,把一个"假 LLM"当节点跑了一遍:

from langgraph.graph import StateGraph, MessagesState, START, END


def mock_llm(state: MessagesState):
    print(state)
    return {"messages": [{"role": "ai", "content": "hi!lisi"}]}


graph = StateGraph(MessagesState)
graph.add_node(mock_llm)
graph.add_edge(START, "mock_llm")
graph.add_edge("mock_llm", END)
graph = graph.compile()

res = graph.invoke({"messages": [{"role": "user", "content": "hi!zhangsan"}]})
print(res)

StateGraph 吃一个状态类型(这里用内置的 MessagesState,本质就是个消息列表),add_node 挂函数,add_edgeSTART 连到节点、节点连到 ENDcompile 一下就能 invoke

这个骨架记住了,后面 case17/18 的 create_agent 本质上就是在这个图上帮你把节点和边都铺好了——你只管填模型和工具。


case17 —— 会干活、有记忆、还带护栏的 Agent

这是三个里最像"产品"的一个。create_agent 一行把模型、工具、系统提示、记忆、中间件全塞进去:



@tool(description="获取指定城市的当前时间,当用户询问时间、现在几点、当前时间等问题时,调用此工具。参数: city,字符串类型,城市")
def get_current_time(city: str) -> str:
    now_str = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
    return f"{city}的当前时间是{now_str}"


@tool(description="获取指定城市的当前天气,当用户询问天气,气温等问题时,调用此工具。参数: city,字符串类型,城市")
def get_current_weather(city: str) -> str:
    weather_data = {
        "北京": "晴天,温度 28°C,湿度 45%,空气质量良",
        "上海": "多云,温度 30°C,湿度 70%,空气质量优",
        "广州": "阵雨,温度 32°C,湿度 85%,空气质量良",
        "深圳": "晴天,温度 31°C,湿度 65%,空气质量优",
        "杭州": "阴天,温度 27°C,湿度 60%,空气质量良",
    }
    weather_res = weather_data.get(city, f"未找到{city}的天气数据,请尝试查询: 北京、上海、广州、深圳、杭州")
    return f"{city}的当前天气是{weather_res}"


@tool(description="当你需要转账的时候,调用此工具。transfer_from 转账人; transfer_to 收款人; transfer_amount 转账金额 float")
def transfer(transfer_from: str, transfer_to: str, transfer_amount: float) -> str:
    return f"{transfer_from}向{transfer_to}转账{transfer_amount}元"


tools = [get_current_time, get_current_weather, transfer]

system_prompt = """
你是一个智能助手
你可以使用工具来解决问题
工具列表:
1:当用户询问时间、现在几点、当前时间等问题时,调用get_current_time工具
2:当用户询问天气,气温等问题时,调用get_current_weather工具
3:当用户需要转账的时候,调用transfer工具
请根据用户的问题,自主决定是否调用工具,调用哪个工具,调用顺序
如果不需要工具,直接回答用户的问题
"""

checkpointer = InMemorySaver()

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt=system_prompt,
    checkpointer=checkpointer,
    debug=True,
    middleware=[
        SummarizationMiddleware(model=llm, trigger=("tokens", 3000), keep=("messages", 10)),
        HumanInTheLoopMiddleware(interrupt_on={"transfer": True}, description_prefix="需要人工审批才能执行"),
        ModelRetryMiddleware(max_retries=3),
        ToolRetryMiddleware(max_retries=3),
        ModelCallLimitMiddleware(run_limit=10),
    ],
)

thread_id = "user_001"
config = {"configurable": {"thread_id": thread_id}}

res2 = agent.invoke(
    {"messages": [{"role": "user", "content": "我是李四,帮我给张三转100元"}]},
    config=config,
)
print(res2)

几个值得记的点:

  • 工具就是加了 @tool 装饰器的普通函数description 写得越清楚,模型越知道什么时候该调。转账那个工具我没真接银行,返回一句文案演示流程。
  • checkpointerInMemorySaver,配合 thread_id 隔离会话:同一个 thread_id 多轮对话共享上下文,不同 id 互不干扰。这是 agent 的"短期记忆"。
  • middleware 是这文件真正的精华,分四类:
    • SummarizationMiddleware:对话超 3000 token 自动摘要,上下文不会无限膨胀。
    • HumanInTheLoopMiddleware + interrupt_on={"transfer": True}:转账这种危险动作直接卡住等人审,模型不能自己拍板。
    • ModelRetryMiddleware / ToolRetryMiddleware:模型或工具调用挂了自动重试 3 次,网络抽风不至于整个崩。
    • ModelCallLimitMiddleware(run_limit=10):单次会话最多调 10 次模型,纯纯的防烧钱开关。

一句话:工具决定 agent 能干啥,middleware 决定它干得稳不稳、贵不贵、危不危险。


case18 —— 把流程钉死的业务 Agent

case17 是把工具丢给模型自由发挥,case18 反过来,用 system_prompt 把步骤写死,让它严格按流程走。这是个金融分析 agent:


@tool(description="获取指定股票代码的当前价格和涨跌幅。参数:symbol,字符串类型,股票代码,比如:sh600000")
def get_stock_price(symbol: str) -> str:
    price = round(random.uniform(80, 120), 2)
    change = round(random.uniform(-5, 5), 2)
    status = "上涨" if change > 0 else "下跌"
    return f"{symbol} 当前价格: ${price}, 涨跌幅: {change}%, 状态: {status}"


@tool(description="获取指定股票的最新新闻。参数:symbol,字符串类型,股票代码")
def get_stock_news(symbol: str) -> str:
    return f"{symbol} 最新新闻: 公司发布新产品,市场反应积极,分析师上调目标价。"


@tool(description="获取指定公司的最新财报摘要。参数:symbol,字符串类型,股票代码")
def get_financial_report(symbol: str) -> str:
    return f"{symbol} 最新财报: 营收增长 15%,净利润增长 8%,超出市场预期。"


tools = [get_stock_price, get_stock_news, get_financial_report]

system_prompt = """
你是一个专业的金融分析师,负责分析股票市场。
请严格按照以下步骤执行:
1:首先,调用get_stock_price工具查询用户指定的股票
2:然后,根据股票的涨跌幅判断股票的状态:
    如果涨了就调用get_stock_news工具查询股票的最新新闻
    如果跌了就调用get_financial_report工具查询股票的最新财报摘要
3:最后,生成一份报告,报告包括股票的当前价格,涨跌幅,状态,最新新闻,最新财报摘要
"""

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt=system_prompt,
    debug=True,
)

res1 = agent.invoke(
    {"messages": [{"role": "user", "content": "分析一下股票 AAPL 的情况"}]}
)
print(res1)

它的核心是一个条件分支:先查价,涨了查新闻、跌了查财报,最后汇总出报告。分支判断交给模型自己看 change 正负,但下一步调哪个工具是 prompt 里定死的。工具里的股价是 random 模拟的,跑一次结果不一样,正好能看 agent 怎么根据随机结果走不同分支。

这个 case 和 case17 的区别一句话讲清:case17 是"你看着办",case18 是"照着我写的步骤办"。实际项目里,涉及强流程、要合规的活儿(比如先分析 → 再审批 → 出报告)更适合 case18 这种写法。


收尾

三个文件其实是一条线:

  • case19 让你看清 LangGraph 的底子(状态 + 节点 + 边);
  • case17 在上面叠工具和护栏,做出能多轮对话、能卡审批的 agent;
  • case18 再把业务流程写死,做垂直场景的编排。

更多推荐