102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
# requires: langchain, langchain_core, pydantic
|
||
"""LangChain 1.x 基础:LCEL 链、结构化输出解析 + 重试 + 备用模型、create_agent 工具调用 + 中间件。
|
||
|
||
用假模型离线运行(GenericFakeChatModel 按顺序返回预设消息)。换成真实模型:
|
||
from langchain.chat_models import init_chat_model
|
||
model = init_chat_model("openai:gpt-4.1-mini") # 模型名和提供方写法以官方文档为准
|
||
基于 langchain 1.4 / langchain-core 1.7 编写,以官方文档为准:https://docs.langchain.com/oss/python/langchain/overview
|
||
运行:python lc_basics.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from langchain.agents import create_agent
|
||
from langchain.agents.middleware import ModelCallLimitMiddleware
|
||
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
|
||
from langchain_core.messages import AIMessage
|
||
from langchain_core.output_parsers import PydanticOutputParser, StrOutputParser
|
||
from langchain_core.prompts import ChatPromptTemplate
|
||
from langchain_core.runnables import RunnableLambda
|
||
from langchain_core.tools import tool
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
def fake(*texts_or_msgs) -> GenericFakeChatModel:
|
||
msgs = [m if isinstance(m, AIMessage) else AIMessage(content=m) for m in texts_or_msgs]
|
||
return GenericFakeChatModel(messages=iter(msgs))
|
||
|
||
|
||
# ---------- 1. LCEL:提示词 | 模型 | 解析器 ----------
|
||
def demo_lcel() -> str:
|
||
prompt = ChatPromptTemplate.from_messages([
|
||
("system", "你是客服,用一句话回答。"),
|
||
("human", "{question}"),
|
||
])
|
||
chain = prompt | fake("您好,开发票请在订单详情页申请。") | StrOutputParser()
|
||
return chain.invoke({"question": "怎么开发票"})
|
||
|
||
|
||
# ---------- 2. 结构化输出:解析 + 失败重试 + 备用模型 ----------
|
||
class Triage(BaseModel):
|
||
category: str = Field(pattern="^(refund|bug|question)$")
|
||
urgent: bool
|
||
|
||
|
||
def demo_structured() -> Triage:
|
||
parser = PydanticOutputParser(pydantic_object=Triage)
|
||
prompt = ChatPromptTemplate.from_messages([
|
||
("system", "对工单分类。{format_instructions}"),
|
||
("human", "{ticket}"),
|
||
]).partial(format_instructions=parser.get_format_instructions())
|
||
|
||
# 主模型第一次返回了不合法的 JSON,第二次正确:with_retry 对解析失败重试
|
||
primary = prompt | fake("类别是退款", '{"category": "refund", "urgent": true}') | parser
|
||
robust = primary.with_retry(stop_after_attempt=2)
|
||
|
||
# 主链彻底失败时切换到备用链(真实项目中是另一个模型 / 提供方)
|
||
def broken(_):
|
||
raise TimeoutError("主模型超时")
|
||
backup = prompt | fake('{"category": "question", "urgent": false}') | parser
|
||
with_fallback = RunnableLambda(broken).with_fallbacks([backup])
|
||
assert with_fallback.invoke({"ticket": "怎么改地址"}).category == "question"
|
||
|
||
return robust.invoke({"ticket": "我要退款,很急"})
|
||
|
||
|
||
# ---------- 3. create_agent:工具调用 + 调用次数限制 ----------
|
||
class ToolCallingFake(GenericFakeChatModel):
|
||
"""假模型不支持 bind_tools,这里直接返回自身;真实模型会把工具定义发给接口。"""
|
||
def bind_tools(self, tools, **kwargs):
|
||
return self
|
||
|
||
|
||
@tool
|
||
def get_order_status(order_id: str) -> str:
|
||
"""查询订单的物流状态。order_id 形如 A1001。"""
|
||
return {"A1001": "已发货,预计明天送达"}.get(order_id, "订单不存在")
|
||
|
||
|
||
def demo_agent() -> list:
|
||
model = ToolCallingFake(messages=iter([
|
||
AIMessage(content="", tool_calls=[{"name": "get_order_status", "args": {"order_id": "A1001"}, "id": "c1"}]),
|
||
AIMessage(content="您的订单 A1001 已发货,预计明天送达。"),
|
||
]))
|
||
agent = create_agent(
|
||
model,
|
||
tools=[get_order_status],
|
||
system_prompt="你是售后客服,查询订单要用工具。",
|
||
middleware=[ModelCallLimitMiddleware(run_limit=5)], # 防止无限循环
|
||
)
|
||
out = agent.invoke({"messages": [{"role": "user", "content": "A1001 到哪了"}]})
|
||
return out["messages"]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("LCEL:", demo_lcel())
|
||
t = demo_structured()
|
||
print("结构化输出:", t)
|
||
assert t.category == "refund" and t.urgent
|
||
msgs = demo_agent()
|
||
for m in msgs:
|
||
print(f"{type(m).__name__:12s}", m.content or getattr(m, "tool_calls", ""))
|
||
assert [type(m).__name__ for m in msgs] == ["HumanMessage", "AIMessage", "ToolMessage", "AIMessage"]
|