阶段4(下):LangChain 1.x、提示词与注入防护、模型侧(路由/vLLM/LoRA)、AI 编程工具与验证、Dify/Coze 专题

This commit is contained in:
Shikui Wang committed 2026-10-23 16:15:33 +08:00
1 parent caaec830b3
commit 7a32d2c9a6
38 files changed
+3059 -67

No files matched your search

@@ -0,0 +1,103 @@
---
id: ai.langchain.lcel-structured-agent
title: LangChain 1.x:模型和消息、LCEL、结构化输出、create_agent 和中间件
type: lesson
domain: ai
stack: [langchain]
tags: [LangChain, LCEL, init_chat_model, 结构化输出, create_agent, middleware, 重试, 备用模型]
level: intermediate
prerequisites: [ai.function-calling.tool-loop]
est_hours: 3
related: [ai.langgraph.state-graph, ai.langchain.scenario.ticket-triage, ai.langchain.interview]
week: [6, 11]
order: 1
---
# LangChain 1.x:模型和消息、LCEL、结构化输出、create_agent 和中间件
> LangChain 在 2025 年发布 1.0,API 和 0.x 有较大变化(例如 Agent 统一为 `create_agent`,基于 LangGraph 构建,并引入中间件)。网上大量教程是 0.x 写法(`AgentExecutor`、`initialize_agent`、`LLMChain`),不要照抄。本课基于 langchain 1.4 编写,以官方文档为准:https://docs.langchain.com/oss/python/langchain/overview
## 解释说明
### 核心组件
| 组件 | 说明 |
| --- | --- |
| 聊天模型 | 统一接口调用不同提供方:`init_chat_model("提供方:模型名")`;方法 `invoke` / `stream` / `batch` 及异步版本 |
| 消息 | `SystemMessage`、`HumanMessage`、`AIMessage`(可带 `tool_calls`)、`ToolMessage` |
| 提示词模板 | `ChatPromptTemplate.from_messages([...])`,用 `{变量}` 占位 |
| 输出解析器 | `StrOutputParser`、`PydanticOutputParser`;支持原生结构化输出的模型优先用 `model.with_structured_output(Schema)` |
| 工具 | `@tool` 装饰的函数,文档字符串就是工具描述 |
| Agent | `create_agent(model, tools, system_prompt=..., middleware=[...], response_format=...)` |
### LCEL(LangChain 表达式语言)
用 `|` 把组件串成链:`prompt | model | parser`。每个组件都是 Runnable,统一支持 `invoke` / `stream` / `batch` / 异步,并且可以加:
- `.with_retry(stop_after_attempt=3)`:失败重试;
- `.with_fallbacks([备用链])`:失败时切换到备用(例如另一个模型提供方);
- `RunnableParallel`:并行执行多个分支。
### create_agent 和中间件
`create_agent` 实现了标准的工具调用循环(就是第 10 周手写的那个循环),返回一个 LangGraph 图,所以也支持检查点、流式、interrupt。1.x 的**中间件**可以在模型调用 / 工具调用前后插入逻辑,内置的有:调用次数限制、人工审批(HumanInTheLoop)、历史摘要(Summarization)、备用模型(ModelFallback)、PII 检测、工具重试等(名称和参数以官方文档为准)。
**什么时候用 LangChain,什么时候直接用 SDK?** 需要切换多个模型提供方、需要现成的检索 / 工具 / 中间件生态时用 LangChain;逻辑简单、追求最少依赖和完全可控时直接用厂商 SDK。面试时要能说出理由。
## 案例
工单分类服务:用 `PydanticOutputParser`(或 `with_structured_output`)输出 `{category, urgent}`;模型偶尔返回不合法格式,`with_retry` 重试一次;主模型提供方超时时 `with_fallbacks` 切换到备用提供方;批量历史工单用 `batch` 并发处理。
## 代码示例
完整可运行版本(假模型离线运行):[examples/lc-basics/lc_basics.py](examples/lc-basics/lc_basics.py)。
```python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
class Triage(BaseModel):
category: str = Field(pattern="^(refund|bug|question)$")
urgent: bool
parser = PydanticOutputParser(pydantic_object=Triage)
prompt = ChatPromptTemplate.from_messages([
("system", "对工单分类。{format_instructions}"),
("human", "{ticket}"),
]).partial(format_instructions=parser.get_format_instructions())
chain = (prompt | model | parser).with_retry(stop_after_attempt=2).with_fallbacks([prompt | backup_model | parser])
print(chain.invoke({"ticket": "我要退款,很急"})) # category='refund' urgent=True
@tool
def get_order_status(order_id: str) -> str:
"""查询订单的物流状态。order_id 形如 A1001。"""
...
agent = create_agent(model, tools=[get_order_status], system_prompt="你是售后客服。",
middleware=[ModelCallLimitMiddleware(run_limit=5)])
out = agent.invoke({"messages": [{"role": "user", "content": "A1001 到哪了"}]})
```
## 易错点
- 照抄 0.x 教程:`AgentExecutor`、`LLMChain` 等在 1.x 中已移到 `langchain-classic` 或不再推荐。
- 各模型提供方是独立的包(`langchain-openai`、`langchain-anthropic` 等),版本要和 `langchain-core` 匹配。
- 结构化输出只靠提示词约束,不做校验。
- `with_fallbacks` 的备用链输入输出格式和主链不一致。
- 工具的文档字符串写得太简单,模型不知道何时调用。
- 过度封装:简单的一次调用也套很多层链,调试困难。
## 面试常问
- LangChain 1.x 和 0.x 有什么变化?
- LCEL 是什么?有什么好处?
- 怎么保证结构化输出可靠?
- 什么时候不用 LangChain?
@@ -0,0 +1,101 @@
# 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"]
+30 -3
View File
@@ -4,12 +4,39 @@ title: LangChain
type: index
domain: ai
stack: [langchain]
tags: []
tags: [LangChain, LCEL, 结构化输出, Agent]
week: [6, 11]
---
# LangChain
模型与消息、LCEL、工具、结构化输出。
大模型应用框架。BOSS直聘 2026-10-23 的调研中约 9 个岗位提到 LangChain / LangGraph / AutoGen / LlamaIndex。注意 1.x 和 0.x 差异很大,学习时以官方文档为准。
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
## 学习路线
| 顺序 | 课程 | 级别 | 预计 |
| --- | --- | --- | --- |
| 1 | [LangChain 1.x:模型和消息、LCEL、结构化输出、create_agent 和中间件](01-lcel-structured-agent.md) | 进阶 | 3 小时 |
## 企业场景
- [工单自动分类服务:结构化输出、批处理、重试降级和人工复核](scenarios/ticket-triage.md)
## 面试题
- [LangChain 面试题](interview.md)
## 示例代码
- [examples/lc-basics](examples/lc-basics/):LCEL 链、`PydanticOutputParser` + `with_retry` + `with_fallbacks`、`create_agent` 工具调用 + `ModelCallLimitMiddleware`(假模型离线运行)
## 在 16 周计划中
- [第 6 周:LangChain 对照实现](../../../plan/week06-rag-2.md)、[第 11 周:LangGraph 多 Agent](../../../plan/week11-agent-m2-langgraph.md)
## 官方资料
以官方文档为准:
- https://docs.langchain.com/oss/python/langchain/overview
- 1.0 迁移说明:https://docs.langchain.com/oss/python/migrate/langchain-v1
+61
View File
@@ -0,0 +1,61 @@
---
id: ai.langchain.interview
title: LangChain 面试题
type: interview
domain: ai
stack: [langchain]
tags: [LangChain, LCEL, 结构化输出, 面试]
level: intermediate
related: [ai.langchain.lcel-structured-agent]
week: [6, 11, 16]
---
# LangChain 面试题
## 基础
### 【基础】LangChain 是什么?主要组件有哪些?
**参考答案**:构建大模型应用的框架,提供统一的模型接口、消息、提示词模板、输出解析、工具、检索组件、Agent。1.x 中 Agent 统一为 `create_agent`(基于 LangGraph),并通过中间件扩展。
**追问**:LangChain、LangGraph、LangSmith 的关系?→ LangChain 提供组件和高层 Agent;LangGraph 是底层的有状态编排框架;LangSmith 是追踪、评测和监控平台。
### 【基础】LCEL 是什么?
**参考答案**:LangChain 表达式语言,用 `|` 把 Runnable 组件组合成链,统一支持同步 / 异步、流式、批处理,以及重试、备用、并行等能力。
**追问**:`batch` 和循环调用 `invoke` 有什么区别?→ `batch` 会并发执行(可以用 `max_concurrency` 控制并发数),速度更快;循环是串行的。
### 【基础】怎么让模型输出结构化数据?
**参考答案**:优先使用模型原生的结构化输出(`with_structured_output(Schema)`,底层用工具调用或 JSON Schema 模式);不支持时用提示词 + `PydanticOutputParser`。无论哪种都要校验,失败时重试或降级。
**追问**:Schema 设计要注意什么?→ 用枚举限制取值、字段加描述、可选字段明确、不要嵌套太深;字段名和描述模型都会读。
## 进阶
### 【进阶】LangChain 1.x 和 0.x 有什么主要变化?
**参考答案**:Agent 统一为 `create_agent`,基于 LangGraph 构建,支持检查点、流式和人工介入;引入中间件机制(调用限制、摘要、人工审批、备用模型等);旧的 `AgentExecutor`、`LLMChain` 等移到 `langchain-classic`;核心包更精简。具体以官方迁移文档为准。
**追问**:项目里遇到过版本问题吗?→ 按实际经历讲(例如教程是 0.x 写法、包版本不匹配),以及怎么解决(锁定版本、读迁移文档、写测试)。
### 【进阶】怎么做模型的重试和降级?
**参考答案**:Runnable 的 `with_retry` 处理临时错误;`with_fallbacks` 在主链失败后切换到备用模型或提供方;1.x 的 Agent 也可以使用备用模型中间件。降级的最后一层通常是“返回友好提示 / 转人工”。
**追问**:备用模型效果差一些怎么办?→ 评测备用模型在关键用例上的表现,可以只对部分功能降级;记录降级发生的比例并告警。
## 场景题
### 【场景题】做一个工单自动分类服务,你会怎么设计?
**参考答案**:结构化输出(枚举类别、格式校验)+ 重试 + 备用提供方;低置信度人工复核和抽检;异步执行不阻塞工单创建;历史数据批处理控制并发、断点续跑;记录模型和提示词版本;评测集验证准确率。详见[工单分类场景](scenarios/ticket-triage.md)。
**追问**:怎么衡量分类效果?→ 用人工标注的样本计算准确率和各类别的召回率,特别关注高风险类别(投诉)的召回。
### 【场景题】团队里有人说“LangChain 太重了,不如直接调 SDK”,你怎么看?
**参考答案**:各有适用场景。简单调用、单一模型提供方、追求可控性,直接用 SDK;需要多提供方切换、检索 / 工具 / Agent 生态、中间件和 LangGraph 编排能力时,LangChain 能节省大量工作。关键是封装要适度,并锁定版本、写测试应对版本变化。
**追问**:如果用 SDK,怎么保留切换模型的能力?→ 自己定义一层薄的模型接口(或使用 OpenAI 兼容接口),业务代码不直接依赖某个厂商的 SDK。
@@ -0,0 +1,92 @@
---
id: ai.langchain.scenario.ticket-triage
title: 工单自动分类服务:结构化输出、批处理、重试降级和人工复核
type: scenario
domain: ai
stack: [langchain, fastapi, postgresql]
tags: [结构化输出, 批处理, 重试, 降级, 人工复核, 成本]
level: intermediate
prerequisites: [ai.langchain.lcel-structured-agent]
est_hours: 2.5
related: [ai.langchain.interview, ai.llm-serving.index]
week: [6, 11]
---
# 工单自动分类服务:结构化输出、批处理、重试降级和人工复核
## 业务背景与问题
客服每天收到约 5000 张工单,人工分派到退款、技术、物流、投诉等 8 个队列,平均分派耗时 20 分钟。希望用大模型自动分类、判断紧急程度、提取订单号。试运行问题:
1. 模型偶尔输出“类别:退款”这样的自然语言,解析失败;
2. 模型给出了不存在的类别“售后”;
3. 高峰期模型接口限流,工单堆积;
4. 历史 50 万张工单要回填分类,逐条调用太慢、太贵;
5. 分类错误的工单被分到错误的队列,没人发现。
## 约束
- 分派延迟目标:1 分钟内。
- 分类结果要可追溯(模型版本、提示词版本)。
## 方案设计
1. **结构化输出**:用 Schema 定义输出(类别枚举、紧急程度、订单号格式),优先使用模型的原生结构化输出(`with_structured_output`),并用 pydantic 校验;校验失败重试一次,仍失败进入“待人工分派”队列。
2. **置信度和人工复核**:输出中包含置信度(或用规则判断:订单号缺失、类别为“其他”),低置信度进入人工复核;每天抽检 2% 自动分类的工单。
3. **重试和降级**:限流 / 超时用指数退避重试;主提供方持续失败时切换备用提供方;都失败时进入人工队列,不阻塞工单创建。
4. **批处理**:历史回填走离线任务,用 `batch` 控制并发(或厂商的批处理接口,通常更便宜,以厂商文档为准),按天分片、断点续跑。
5. **可追溯**:每条结果记录模型名、提示词版本、原始输出、耗时。
## 关键代码
```python
class Triage(BaseModel):
category: Literal["refund", "tech", "logistics", "complaint", "invoice", "account", "presale", "other"]
urgency: Literal["low", "normal", "high"]
order_id: str | None = Field(default=None, pattern=r"^A\d{4,}$")
confidence: float = Field(ge=0, le=1)
classifier = (
prompt | primary_model.with_structured_output(Triage)
).with_retry(stop_after_attempt=2).with_fallbacks([prompt | backup_model.with_structured_output(Triage)])
async def triage(ticket: Ticket) -> Triage | None:
try:
result = await classifier.ainvoke({"title": ticket.title, "body": ticket.body[:2000]})
except Exception:
await queue.put_manual(ticket.id, reason="模型失败")
return None
if result.confidence < 0.7 or result.category == "other":
await queue.put_review(ticket.id, result)
await save_result(ticket.id, result, model=MODEL_NAME, prompt_version=PROMPT_VERSION)
return result
# 历史回填:控制并发
results = await classifier.abatch(inputs, config={"max_concurrency": 8}, return_exceptions=True)
```
基础写法的可运行示例:[examples/lc-basics](../examples/lc-basics/)。
## 取舍
| 选择 | 优点 | 缺点 |
| --- | --- | --- |
| **大模型分类(本方案)** | 新增类别只改提示词和 Schema;能同时提取信息 | 有成本和延迟;需要评测和复核 |
| 传统文本分类模型 | 便宜、快 | 需要标注数据训练,类别变化要重新训练 |
| 关键词规则 | 简单可解释 | 覆盖率低 |
| 规则 + 大模型 | 明确的用规则(便宜),模糊的用模型 | 两套逻辑要维护 |
## 易错点
- 让模型自由输出类别名,再用字符串匹配。
- 模型失败时阻塞工单创建:分类应该是异步的,失败可降级为人工。
- 回填时并发过高触发限流,或者没有断点续跑,失败后从头再来。
- 模型自报的置信度并不可靠,要和评测集的实际准确率对照校准。
- 换模型或改提示词后没有跑评测集。
## 面试怎么讲
> “工单分类我用结构化输出,类别是枚举、订单号有格式校验,失败重试一次,再失败进人工队列,不阻塞工单创建。主提供方限流时自动切换备用提供方。低置信度和‘其他’类进入人工复核,每天再抽检 2%。历史回填用批处理控制并发、断点续跑。每条结果记录模型和提示词版本,改动前后跑评测集对比准确率。”