LangChain 学习笔记

LangChain 学习笔记

一、全景与模型接入

LangChain 全家桶的分工LangChain 是 LLM 应用开发的工具箱(模型封装、工具、文档加载器等,RAG 是它能干的活之一);LangGraph 是底层持久化运行时引擎,负责状态管理、循环控制和多 Agent 协同;LangSmith 提供可观测性(Tracing)、调试与评估。收费:LangChain 与 LangGraph 开源免费(MIT);LangSmith 有免费 Developer 套餐(1 个席位、每月 5000 条 traces),个人学习够用。

对于我们开发者,我们只需关心第二部分(LangChain处理逻辑即可)。

在这里插入图片描述

这张架构图从下往上看三层——最底下是用户/开发者(提问的人);往上是提示词层(把用户的问题组织成 Prompt,可以套提示词模板);中间是 LLM/模型层(LangChain 处理逻辑所在,负责调用各家模型 API);最上面是输出层(模型返回结果,用输出解析器把响应整理成想要的格式)。请求沿着"用户 → 提示词 → 模型 → 输出"逐层向上,响应再逐层返回。所谓"只需关心第二部分":因为提示词模板、模型调用、输出解析这些 LangChain 都提供了现成封装,开发者主要的编码工作就在这一层里组装它们。

模型包装器

在这里插入图片描述

LangChain 把"模型"包装成统一对象,分两类——上面是 chat_models(对话模型),代表是 ChatOpenAI:输入是消息列表(角色+内容),输出是 AIMessage 对象(用 .content 取文本),这是现在的主流用法;下面是传统 LLM(补全模型):老接口,输入一段文字、输出纯文本,现在基本不用了。最底下是各厂商的集成包langchain-openailangchain-qwenlangchain-community 等)——同一个 ChatOpenAI 类换 base_url 就能连任何 OpenAI 兼容服务,换成厂商专用包则能用到该厂商的独有功能。

模型接入
"""
LangChain 初体验 —— 传统方式 vs LangChain 方式调用大模型
"""

# ============================================================
# 导入需要用到的工具
# ============================================================
import os                                # Python 自带,读环境变量
from dotenv import load_dotenv           # 读取 .env 文件
from openai import OpenAI                # 传统方式:openai 官方 SDK 的客户端类
from langchain_openai import ChatOpenAI  # LangChain 方式:模型封装类

# ============================================================
# 从 .env 读取 API 配置
# ============================================================
load_dotenv()                                      # 把 .env 里的 KEY=VALUE 装进环境变量
API_BASE_URL = os.environ.get("API_BASE_URL", "")  # API 服务地址
API_KEY = os.environ.get("API_KEY", "")            # API 密钥
MODEL_NAME = os.environ.get("MODEL_NAME", "")      # 模型名

QUESTION = "你是谁?"   # 两种方式问同一个问题,方便对比输出

# ============================================================
# 传统方式跟大模型交互
# ============================================================
print("=" * 50)   # 打印 50 个等号做分隔线(* 是字符串重复运算符:"=" 重复 50 次)
print("① 传统方式:openai SDK 直接调用")
print("=" * 50)

# 创建大模型连接
client = OpenAI(
    api_key=API_KEY,        # 密钥
    base_url=API_BASE_URL,  # 服务地址:指哪连哪,不是只能 OpenAI 官方
)

completion = client.chat.completions.create(           # 发起对话请求
    model=MODEL_NAME,                                  # 用哪个模型
    messages=[{
   
   "role": "user", "content": QUESTION}],  # 消息列表(角色+内容)
)
print(completion.choices[0].message.content)  # choices[0]:候选答案列表的第 1 个(下标从 0 开始数)

# ============================================================
# LangChain 方式
# ============================================================
print("\n" + "=" * 50)
print("② LangChain 方式:ChatOpenAI + invoke")
print("=" * 50)

# 同样三个参数,把同一个服务包装成 LangChain 的模型对象
llm = ChatOpenAI(
    model=MODEL_NAME,        # 模型名
    base_url=API_BASE_URL,   # 服务地址:LangChain 不锁官方,一样指哪连哪
    api_key=API_KEY,         # 密钥
)

response = llm.invoke(QUESTION)   # invoke: 调用大模型,向大模型提问,并得到回答
print(response.content)           # .content:从 AIMessage 对象里取出答案文本

在这里插入图片描述

运行结果截图——上半部分是①传统方式的输出(openai SDK 打印出模型的自述回答),下半部分是②LangChain 方式的输出(llm.invoke + .content 打印出同一模型的回答)。两种方式都正常拿到了答案,证明接入成功。

运行结果(python LangChain初体验.py 实测输出,LLM 回答每次略有不同,此为一次实际运行):

==================================================
① 传统方式:openai SDK 直接调用
==================================================

你好!我是 Qwen3.5,是通义千问系列中最新推出的大语言模型。我由通义实验室研发,支持全球 100 多种语言,
具备强大的逻辑推理、代码生成、长文档分析、多模态处理等能力。需要我帮你解决什么问题吗? 😊

==================================================
② LangChain 方式:ChatOpenAI + invoke
==================================================


你好!我是 **Qwen3.5**,是阿里巴巴集团通义实验室研发的超大规模语言模型。

我擅长文本理解、逻辑推理、代码生成以及多语言交互等。我可以帮你写故事、写公文、写邮件、写剧本,
进行逻辑推理、编程,或者解答各种问题。

有什么想聊的或者需要帮忙的吗?尽管告诉我!

小结: 两种方式连的是同一个服务、同一个模型、问的同一个问题,都自称 Qwen3.5、答案方向一致,说明 LangChain 并不要求官方模型—— base_url 指哪连哪。
区别在写法: 传统方式手动管消息列表、一层层取结果;LangChain 把模型封装成统一对象,invoke 一行调用,后面还能无缝接提示词模板、输出解析器、RAG 链。

厂商专用接口(以千问为例)

当然还有一种方式就是使用LangChain它api里面含有的模型厂商接口,比如千问,如图所示:

在这里插入图片描述

装厂商集成包 pip install -U langchain-qwen,然后 from langchain_qwen import QwenChat,直接创建千问模型对象(配 api_key,需要去阿里云百炼申请 DASHSCOPE_API_KEY)。

三种接入方式怎么选:

方式 写法 适用场景
① openai SDK + base_url OpenAI(base_url=...) 不想多装包,任何 OpenAI 兼容服务
② LangChain ChatOpenAI + base_url ChatOpenAI(base_url=...) 同上,且后续要接 LangChain 的链/Agent(本项目主要用这种)
③ 厂商专用包 from langchain_qwen import QwenChat 要用该厂商独有功能(如千问的原生参数)

本质上三者殊途同归:只要服务兼容 OpenAI 协议,方式②一个类通吃;厂商专用包是把"官方 SDK"翻译成 LangChain 接口的适配层,不装它照样能连。

二、消息与提示词(输入侧)

消息 Message 的三种写法

对话的最小单位是消息(Message),每条消息有一个角色SystemMessage(系统设定/人设)、HumanMessage(用户说的话)、AIMessage(模型说的话)。把多条消息组成列表喂给模型,就是一段完整对话。写法有三种:

  1. 消息对象(LangChain 原生):SystemMessage(content="...") 这样的类实例,类型安全、IDE 有提示;
  2. 字典格式(OpenAI SDK 风格):{"role": "system", "content": "..."},来自其他 openai 代码迁移时最顺手;
  3. 消息元数据additional_kwargs={...} 挂在消息上的"行李标签"——模型不读它,但程序可以随消息流转存取(用户ID、时间戳、渠道等业务数据)。
"""
消息Message三种写法 —— 消息对象 / 字典格式 / 消息元数据
"""

# ============================================================
# 导入需要用到的工具
# ============================================================
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage   # 三种消息类
from model import llm                                   # 从专门的模型脚本导入配好的模型实例(连接+参数)

# ============================================================
# 第 1 步:方式一——消息对象(LangChain 原生写法)
#          SystemMessage=系统设定(人设),HumanMessage=用户说的话,AIMessage=模型说的话
#          典型场景:预设一段"模型已经回答过一轮"的历史,让模型接着聊
# ============================================================
messages_v1 = [
    SystemMessage(content="你是一个专业的Python翻译助手,把用户给的代码翻译成一句中文说明。"),
    HumanMessage(content="def greet(): pass"),
    AIMessage(content="这段代码定义了一个名为 greet 的函数,函数体为空。"),
    HumanMessage(content="x = [i**2 for i in range(10)]"),
]

print("=" * 50)   # 打印 50 个等号做分隔线(* 是字符串重复运算符:"=" 重复 50 次)
print("方式一:消息对象(SystemMessage/HumanMessage/AIMessage)")
print("=" * 50)
response_v1 = llm.invoke(messages_v1)        # 消息列表直接喂给模型
print("模型接着上一轮回答:", response_v1.content)   # 模型会扮演翻译助手接着翻译新代码

# ============================================================
# 第 2 步:方式二——字典格式(OpenAI SDK 风格)
#          用 {"role": 角色, "content": 内容} 描述每条消息,LangChain 自动转换成消息对象
# ============================================================
messages_v2 = [
    {
   
   "role": "system", "content": "你是一个专业的Python翻译助手,把用户给的代码翻译成一句中文说明。"},
    {
   
   "role": "user", "content": "def greet(): pass"},
    {
   
   "role": "assistant", "content": "这段代码定义了一个名为 greet 的函数,函数体为空。"},
    {
   
   "role": "user", "content": "x = [i**2 for i in range(10)]"},
]

print("\n" + "=" * 50)
print("方式二:字典格式(role/content 键值对)")
print("=" * 50)
response_v2 = llm.invoke(messages_v2)        # 字典列表也能直接喂,LangChain 自动转成消息对象
print("模型回答:", response_v2.content)

# ============================================================
# 第 3 步:方式三——消息元数据(additional_kwargs)
#          挂在消息上的"程序内部数据":模型不读它,但你的代码可以取出来用
#          典型用途:标注消息来源、时间戳、业务ID等,随消息一起流转
# ============================================================
msg = HumanMessage(
    content="你好",                          # content:消息正文(模型只看这个)
    additional_kwargs={
   
                         # additional_kwargs:附加元数据(模型忽略,程序自用)
        "user_id": "user-001",               # 谁发的消息
        "timestamp": "2026-08-21 22:00",     # 发送时间
        "channel": "web",                    # 来自哪个渠道
    },
)

print("\n" + "=" * 50)
print("方式三:消息元数据(additional_kwargs)")
print("=" * 50)
print("消息正文(模型读的):", msg.content)
print("元数据(模型不读,程序可用):", msg.additional_kwargs)
response_v3 = llm.invoke([msg])              # 元数据随消息发给框架,但不会进提示词
print("模型只回应正文:", response_v3.content)

# ============================================================
# 附:混用——同一个列表里消息对象和字典混编(LangChain 自动统一处理)
# ============================================================
messages_mix = [
    SystemMessage(content="你是一个简洁的助手,回答不超过15个字。"),   # 对象写法
    {
   
   "role": "user", "content": "1+1等于几?"},                         # 字典写法
]

print("\n" + "=" * 50)
print("附:混用(消息对象 + 字典在同一个列表里)")
print("=" * 50)
response_mix = llm.invoke(messages_mix)      # 混编列表直接喂,LangChain 自动统一成消息对象
print("模型回答:", response_mix.content)

# ============================================================
# 小结
# ============================================================
print("\n" + "=" * 50)
print("小结:① 方式一和方式二完全等价(对象版/字典版),按团队习惯选;")
print("     ② 方式三不改变模型行为,是给程序自己挂的'行李标签';")
print("     ③ 对象和字典可以混编在同一个列表里(上面实测),LangChain 都认。")

运行结果(python 消息三种写法.py 实测输出,LLM 回答每次略有不同,此为一次实际运行):

==================================================
方式一:消息对象(SystemMessage/HumanMessage/AIMessage)
==================================================
模型接着上一轮回答: 

这段代码使用列表推导式生成一个列表 x,其中包含 0 到 9 的平方。

==================================================
方式二:字典格式(role/content 键值对)
==================================================
模型回答: 

这段代码使用列表推导式创建一个包含 0 到 9 平方数的列表,并赋值给变量 x。

==================================================
方式三:消息元数据(additional_kwargs)
==================================================
消息正文(模型读的): 你好
元数据(模型不读,程序可用): {'user_id': 'user-001', 'timestamp': '2026-08-21 22:00', 'channel': 'web'}
模型只回应正文: 

你好!👋 很高兴为你服务。

请问今天有什么我可以帮你的吗?无论是回答问题、协助写作、翻译,还是随便聊聊,我都在这里!

==================================================
附:混用(消息对象 + 字典在同一个列表里)
==================================================
模型回答: 

1+1 等于 2。

==================================================
小结:① 方式一和方式二完全等价(对象版/字典版),按团队习惯选;
     ② 方式三不改变模型行为,是给程序自己挂的'行李标签';
     ③ 对象和字典可以混编在同一个列表里(上面实测),LangChain 都认。

运行结果解读

  • 方式一 vs 方式二:两段完全相同的对话(人设+历史+新问题),一个用对象写、一个用字典写,模型给出的翻译几乎一样——等价的直接实证;
  • AIMessage 的用途:历史里预置一条"模型已经回答过",模型就会接着这个身份继续聊(这就是多轮对话的本质:把历史全量重发);
  • 方式三:元数据 additional_kwargs 原样存取(打印分毫不差),模型只回应了正文"你好",完全没提 user_id 那些——证明它确实不进提示词;
  • 混用:对象和字典同列表,模型正常回答且遵守了 system 的"15字"约束——LangChain 在入口统一做了转换。

和前面"invoke 的输入"小节那张三种等价写法表对上了:那张表里的"元组写法"是字典/对象之外的第三种简写,本节把它们全部展开成了完整 demo。

提示词模板

提示词模板解决什么问题:提示词里"结构固定、只有几个点会变"是常态(角色设定、场景、任务各占一个坑)。手拼 f-string 的问题是——变量一多就容易拼错、没法复用、也没有校验。ChatPromptTemplate 的解法是把提示词做成填空题:会变的部分全部挖成 {占位变量},调用时用字典填空,同一个模板反复复用。

比喻:模板是公文表格,占位变量是表格里的空格——填什么内容随你,表格的格式(骨架)永远不变;f-string 手拼相当于每次都重新画一张表格。

基本用法三步:from_template(模板字符串) 做模板 → .invoke(字典) 填空 → 结果直接喂 llm.invoke(...)(模板和模型无缝衔接)。

为什么模板字符串要用三个引号? 普通引号 "..." 只装得下一行,中间直接按回车会报 SyntaxError(想在里面换行只能手写 \n,又长又难读);三引号 """...""" 允许跨行书写,换行符原样保留在字符串里——实测三引号版和手动 \n 版用 == 比较为 True,完全等价,三引号就是多行文本的便捷写法('''...''' 三个单引号同理)。另外三引号在代码里有两种身份:赋值给 TEMPLATE 变量是普通多行字符串,写在函数体第一行是文档字符串(docstring)——和 @ 一样:语法相同,身份看位置。

"""
提示词模板初体验 —— ChatPromptTemplate:把提示词做成"填空题"
"""

# ============================================================
# 导入需要用到的工具
# ============================================================
from langchain_core.prompts import ChatPromptTemplate   # 对话提示词模板类:带占位变量 {xxx} 的填空题
from model import llm                                   # 从专门的模型脚本导入配好的模型实例(连接+参数)

# ============================================================
# 第 1 步:定义模板——结构固定,会变的部分全部抽成占位变量
# ============================================================
# 模板字符串里的 {character} 等是占位符(挖好的空),调用时才填入具体值;
# 同一个模板可以反复用不同值填空,生成不同角色的提示词
TEMPLATE = """你是一个{character},正在{scenario}中与人交流。
请用{language_level}的中文,帮我完成以下任务:
{task}"""

prompt = ChatPromptTemplate.from_template(TEMPLATE)   # 把字符串变成模板对象

# ============================================================
# 第 2 步:填空生成提示词——.invoke(字典) 给每个占位符填值
# ============================================================
messages = prompt.invoke({
   
   
    "character": "资深的Python工程师",        # 填入 {character} 这个空:角色
    "scenario": "公司的技术分享会",            # 填入 {scenario} 这个空:场景
    "language_level": "通俗易懂",              # 填入 {language_level} 这个空:语言水平
    "task": "解释什么是列表推导式,并给一个例子",   # 填入 {task} 这个空:具体任务
})

print("=" * 50)   # 打印 50 个等号做分隔线(* 是字符串重复运算符:"=" 重复 50 次)
print("填空后的完整提示词(发给模型的消息列表):")
print("=" * 50)
print(messages)   # 打印填空结果:填好的消息(单段模板 → 一整条 HumanMessage,占位符已全部替换成值)

# ============================================================
# 第 3 步:把填好空的消息交给模型回答
# ============================================================
print("\n" + "=" * 50)
print("模型的回答:")
print("=" * 50)
response = llm.invoke(messages)   # 模板.invoke 的结果可以直接喂给 llm.invoke(无缝衔接)
print(response.content)           # .content:从 AIMessage 对象里取出答案文本

# ============================================================
# 第 4 步:同一个模板换组值再填——体现"模板可复用"
# ============================================================
print("\n" + "=" * 50)
print("换个角色再填一次(模板复用):")
print("=" * 50)
messages2 = prompt.invoke({
   
   
    "character": "唐朝诗人",                  # 换一个角色:诗人
    "scenario": "长安茶馆的闲聊",              # 换一个场景:茶馆
    "language_level": "文雅",                 # 换一种语言水平
    "task": "解释什么是列表推导式,并给一个例子",   # 任务不变
})
response2 = llm.invoke(messages2)   # 再喂给模型
print(response2.content)

运行结果(python 提示词模板初体验.py 实测输出,LLM 回答每次略有不同,此为一次实际运行。注:外层用四个反引号,因为模型回答里自带三个反引号的代码块):

==================================================
填空后的完整提示词(发给模型的消息列表):
==================================================
messages=[HumanMessage(content='你是一个资深的Python工程师,正在公司的技术分享会中与人交流。\n请用通俗易懂的中文,帮我完成以下任务:\n解释什么是列表推导式,并给一个例子', additional_kwargs={}, response_metadata={})]

==================================================
模型的回答:
==================================================


大家好,我是 [你的名字]。

今天咱们来聊一个 Python 里非常经典,甚至可以说是“标志性”的特性——**列表推导式(List Comprehension)**。

很多刚学 Python 的朋友会觉得它有点像“天书”,但其实它非常直观。作为经常写代码的人,我把它比作是**“写循环的快捷键”**。

### 1. 它是啥?

想象一下,我们想做一个新列表,比如把旧列表里的每个数字都乘 2。

**传统写法(For 循环):**
```python
# 这是老前辈们常用的写法
numbers = [1, 2, 3, 4, 5]
result = []
for n in numbers:
    result.append(n * 2)
print(result)  # 输出:[2, 4, 6, 8, 10]
```
这段代码虽然逻辑清晰,但写了 4 行,还要声明变量、调用 `append` 方法。

**列表推导式写法:**
```python
# 这是 Pythonic 的写法,一行搞定
numbers = [1, 2, 3, 4, 5]
result = [n * 2 for n in numbers]
print(result)  # 输出:[2, 4, 6, 8, 10]
```
看,只需要一行!这就是列表推导式。它把“循环”和“生成”的过程压缩到了一个方括号里。

### 2. 它的公式长啥样?

它的核心结构其实就三句话,用大白话说就是:
**“我要一个列表,里面装的是 [表达式],来源是 [for 循环里的变量],还可以加个 [if 条件]。”**

标准语法:
```python
[ 表达式 for 变量 in 可迭代对象 if 条件 ]
```

### 3. 举个栗子(带条件的)

刚才那个例子只是简单的转换。如果我想“只把偶数乘 2,奇数跳过”呢?

**传统写法:**
```python
numbers = [1, 2, 3, 4, 5]
result = []
for n in numbers:
    if n % 2 == 0:  # 只有偶数才处理
        result.append(n * 2)
```

**列表推导式写法:**
```python
numbers = [1, 2, 3, 4, 5]
result = [n * 2 for n in numbers if n % 2 == 0]
# 输出:[4, 8]
```
看,把 `if` 条件直接放在后面,逻辑依然很顺畅。

### 4. 资深工程师的“避坑”建议

虽然列表推导式很爽,但在咱们实际开发中,我有两点建议想分享给大家:

1.  **别为了炫技而炫技**:如果一行代码里嵌套了两层 `for` 循环,或者 `if` 条件太复杂,**千万别用**。这时候,还是老老实实写 `for` 循环更清晰。代码是给人看的,不是给机器跑的。
2.  **注意可读性**:如果这段代码是写给别人看,或者半年后你自己看,确保你能一眼看懂它在干什么。

### 总结一下

列表推导式就是:**用更少的代码,实现列表的生成、转换和过滤。**

它能让你的代码看起来更“地道”(Pythonic),也能减少一些重复的样板代码。下次写循环生成列表的时候,不妨试着用一下它,但记得,**清晰永远比短小更重要**。

以上就是我的分享,希望能帮到大家!有问题的咱们随时交流。

==================================================
换个角色再填一次(模板复用):
==================================================


足下且慢,且饮这盏清茶。

你问起这“列表推导式”,在下虽身处大唐,然观今人奇术,亦觉妙趣横生。这玩意儿,恰似我辈诗家炼字,亦如算经中简化的术法。

所谓列表推导式,便是以寥寥数语,概括万千之意。

若用寻常之法,欲从旧列之中,依某法生出新列,需得一行行循行,一笔笔记录,繁复冗长。而此法,则如律令般,只立一简策,便自动生出新列。

好比织锦,旧法需一根根引线,此法只需一令,丝线便依纹样自动织就。

**举个浅显的例子:**

若足下欲求一数,得一列新数,皆由旧数“一”至“五”,各取其一,再乘其自身(即求平方)。

旧法需言:
“取一数,记之,乘自身,存之,再取一数……"

而列表推导式,只需这般写道:
`[x**2 for x in range(1, 6)]`

**此中奥妙,在下试着解与足下听:**
*   **方括号** `[]` 者,乃盛新列之锦囊也。
*   **x** 者,乃遍历旧列之行者。
*   **x** **2** 者,乃所行之事,即求其平方。
*   **for** **in** 者,乃行路之规,自何处至何处。

如此一行,便胜过繁文缛节百句。

这正如诗云:“删繁就简三秋树,领异标新二月花。”今人以此法省却笔墨,足下以为如何?

来,再饮一杯,这茶正暖。

运行结果解读

  • 第一段打印的 messages= 能看到填空后的成品——四个占位符已被替换成具体值,说明模板确实只干了"填空"这一件事;
  • 同一个任务(解释列表推导式),工程师版开场"大家好"全程 Markdown 讲课腔,诗人版"足下且慢,且饮这盏清茶"全程茶馆古风——四个变量一换,输出风格天差地别,这就是"模板复用"最直观的证据;
  • prompt.invoke(...) 的返回值直接喂给 llm.invoke(...),中间不需要任何转换——模板和模型生来就是接得上的(后面学 LCEL 链时,这个"无缝衔接"会成为流水线的一环:模板 | 模型 | 输出解析器)。

系统提示词:静态与动态

系统提示词是什么:还记得「消息的三种写法」里消息的角色吗——SystemMessage 是"系统设定/人设",它在对话开始前告诉模型"你是谁、该怎么说话"。create_agent 把它做成了一个参数:静态版一句话写死,图例就是 create_agent(system_prompt="你是一个AI助手,可以回答任何问题")——出厂就印在名牌上,谁来都这一句。

动态版解决什么问题:同一个 agent,新手来问要说人话、专家来问要给硬货——提示词得看人下菜。这没法用静态参数做到(它是在组装时就固定了的),得靠中间件在每次调用时现算。图例用的是 @dynamic_prompt 装饰器写法:

比喻:静态提示词是印死的工牌;动态提示词是前台接待员——来客先出示证件(context),接待员看证件上的身份(user_role)临时发不同级别的工牌(系统提示词)。

机制三件套(证件怎么递到接待员手里):

  1. class Context(TypedDict): user_role: str —— 声明"随身信息卡"的格式:卡上有哪些字段、各是什么类型(TypedDict 不存数据,只是一张格式模板,和填空题的 {占位变量} 一个思路);
  2. create_agent(..., context_schema=Context) —— 把格式登记给 agent(不登记,invoke 传 context 会被拒收);
  3. agent.invoke({...}, context={"user_role": "beginner"}) —— 每次调用时带卡进门;中间件里用 request.runtime.context 接到这张卡。

@dynamic_prompt 和「中间件与记忆」里的 @wrap_model_call 是什么关系? 前者是后者的特化封装(源码文档原话:用 wrap_model_call 专门为动态提示词造的便捷装饰器)。分工区别:

@wrap_model_call @dynamic_prompt
管什么 管整个请求(可以换模型、改任何东西) 只管一件事:换系统提示词
函数返回 handler(request.override(...)) 把请求递回流水线 直接 return 一个字符串(就是要换上去的提示词)
适合场景 动态切换模型的"深聊换高级模型" 本节的"按角色换人设"

共同点:都挂 middleware=[] 才生效,模型都看不见它们(中间件对模型透明,前文讲过)。

"""
动态系统提示词 —— @dynamic_prompt 中间件:按用户角色(专家/新手)动态生成不同的系统提示词
"""
from typing import TypedDict                      # TypedDict:只用来"声明"一个字典的键和值类型
from langchain.agents import create_agent         # Agent 构建入口
from langchain.agents.middleware import dynamic_prompt, ModelRequest   # 动态提示词装饰器 + 请求类型
from model import llm                             # 配好的模型实例(连接+参数都在 model.py 里)


# 自定义 context 格式:约定"每次调用 agent 时"可以附带哪些运行时上下文字段
# 好比一张"随身信息卡"的模板 —— 本次用户是什么角色,写在卡上带进来
class Context(TypedDict):
    user_role: str   # 用户角色:expert=专家 / beginner=新手 / 不传则默认 user


# @dynamic_prompt:专门用于"动态生成系统提示词"的中间件装饰器(底层仍是 wrap_model_call 的封装)
# 被装饰的函数接收 request、返回一个字符串 —— 这个字符串就会成为本次调用的系统提示词
@dynamic_prompt
def user_role_prompt(request: ModelRequest) -> str:
    """根据用户角色来生成系统提示词."""
    # request.runtime.context:本次调用传入的运行时上下文(就是 invoke 时传的那个字典)
    # 注意:不传 context 时它是 None 而不是空字典,直接 .get 会报错;
    # "or {}" 表示"如果是 None 就换成空字典",让后面的 .get 兜底真正生效
    user_role = (request.runtime.context or {
   
   }).get("user_role", "user")
    # .get("user_role", "user"):取角色字段;字典里没这个键就兜底成 "user"
    base_prompt = "你是一个有用的助手."

    if user_role == "expert":                       # 专家:要详细、偏技术
        return f"{
     
     base_prompt} 提供详细的技术回应."
    elif user_role == "beginner":                   # 新手:要简单、避免行话
        return f"{
     
     base_prompt} 简单地解释概念,避免行话."

    return base_prompt                               # 其他角色:只有基础提示词


# 组装 agent:中间件挂在 middleware=[](身份看挂载口 —— 挂这里是中间件,挂 tools[] 才是工具)
# context_schema=Context:登记刚才的上下文格式,登记后 invoke 才允许传 context
agent = create_agent(
    model=llm,
    middleware=[user_role_prompt],
    context_schema=Context,
)

# 同一个问题,分别用"新手 / 专家 / 不传角色"三种身份调用 —— 系统提示词不同,回答风格就不同
question = "什么是大语言模型?"

print("=" * 20, "第 1 轮:beginner(新手)身份", "=" * 20)
result1 = agent.invoke(
    {
   
   "messages": [{
   
   "role": "user", "content": question}]},   # 对话内容照常放 messages
    context={
   
   "user_role": "beginner"},                       # 运行时上下文:本次调用者是新手
)
print(result1["messages"][-1].content)                       # [-1]:取消息列表最后一条 = 最终回答

print()
print("=" * 20, "第 2 轮:expert(专家)身份", "=" * 20)
result2 = agent.invoke(
    {
   
   "messages": [{
   
   "role": "user", "content": question}]},
    context={
   
   "user_role": "expert"},                        # 同一个问题,换成专家身份
)
print(result2["messages"][-1].content)

print()
print("=" * 20, "第 3 轮:不传 context(默认 user)", "=" * 20)
result3 = agent.invoke(
    {
   
   "messages": [{
   
   "role": "user", "content": question}]},
    # 这次完全不带 context 参数 → runtime.context 为 None → or {} 接住 → 角色兜底成 "user"
)
print(result3["messages"][-1].content)

运行结果(python 动态系统提示词.py 实测输出,LLM 回答每次略有不同,此为一次实际运行):

==================== 第 1 轮:beginner(新手)身份 ====================


简单来说,大语言模型就是一个**读过海量文字,并且学会了如何理解和生成人类语言的智能程序**。

为了让你更容易理解,可以把它想象成一个**超级博学的学生**:

1.  **它读了什么?**
    在学习阶段,它阅读了互联网上几乎所有的公开文字,比如书籍、文章、对话记录等。

2.  **它学到了什么?**
    它学会了词语之间是怎么搭配的。比如,看到“今天天气”,它根据读过的内容,知道后面很可能接“很好”或者“不错”。

3.  **它怎么回答你?**
    当你问它问题时,它不会像查字典一样找答案,而是根据它学到的规律,**像玩“文字接龙”一样,猜出最合适的下一个字和句子**。

**它能帮你做什么?**
*   **聊天**:回答你的问题,陪你说说话。
*   **写作**:帮你写邮件、写故事、写代码。
*   **翻译**:把一种语言变成另一种语言。
*   **总结**:把很长的一篇内容变成简短的要点。

**总结:**
它不是真的像人一样有思想,但它非常擅长模仿人类的说话方式,是一个能帮你处理文字任务的**超级工具**。

==================== 第 2 轮:expert(专家)身份 ====================


大语言模型(Large Language Model,简称 **LLM**)是人工智能领域中一种基于深度学习技术的自然语言处理(NLP)模型。它是当前人工智能技术发展的核心驱动力之一。

以下是对大语言模型的技术性详细解析,涵盖定义、架构、训练流程、核心能力及局限性。

---

### 1. 核心定义

大语言模型是一种**基于神经网络(通常是 Transformer 架构)**,在**海量文本数据**上进行训练,旨在学习语言统计规律、语义结构及世界知识的深度学习模型。

*   **"Large"(大):** 指模型的参数量(Parameters)巨大。现代 LLM 的参数量通常在 **十亿(Billions)** 到 **万亿(Trillions)** 级别。参数量的增加通常与模型表现力呈正相关(Scaling Law)。
*   **"Language"(语言):** 虽然模型主要处理文本(语言),但其能力往往泛化到代码、逻辑推理、多模态数据等领域。
*   **"Model"(模型):** 本质是一个概率生成模型。它根据输入序列(Context),计算下一个词元(Token)出现的概率分布,并据此生成输出。

### 2. 核心架构:Transformer

绝大多数现代大语言模型都基于 **Transformer** 架构(由 Google 在 2017 年的论文《Attention Is All You Need》提出)。相比之前的 RNN 或 CNN,Transformer 解决了长距离依赖和并行计算的问题。

**关键组件:**
1.  **自注意力机制(Self-Attention):** 允许模型在处理当前词元时,关注序列中的其他词元,无论距离多远。这使模型能够理解上下文语境。
2.  **多头注意力(Multi-Head Attention):** 并行运行多个注意力机制,从不同子空间捕捉信息(如语法、语义、指代关系)。
3.  **前馈神经网络(Feed-Forward Networks):** 对注意力输出的特征进行非线性变换和整合。
4.  **残差连接与层归一化(Residual Connections & Layer Norm):** 保证梯度在深层网络中有效传播,防止梯度消失。
5.  **Decoder-only 架构:** 目前主流的 LLM(如 GPT 系列)主要使用 **Decoder-only** 结构,即专注于根据上文预测下文(自回归生成),而非像 BERT 那样使用 Encoder 进行掩码预测。

### 3. 训练流程(Training Pipeline)

大语言模型的训练通常分为三个阶段,这是一个渐进式的过程:

#### 3.1 预训练(Pre-training)
*   **目标:** 学习通用的语言表示和世界知识。
*   **数据:** 互联网上的海量文本(网页、书籍、维基百科、代码库等),数据量达到 TB 或 PB 级别。
*   **任务:** **自回归语言建模(Autoregressive Language Modeling)**。即给定序列 $X_{1:n}$,预测下一个词元 $X_{n+1}$ 的概率:$P(X_{n+1} | X_{1:n})$。
*   **结果:** 模型掌握了语法、事实、逻辑推理的初步能力,但可能无法很好地遵循人类指令。

#### 3.2 有监督微调(Supervised Fine-Tuning, SFT)
*   **目标:** 让模型学会遵循指令(Instruction Following)。
*   **数据:** 由人类专家编写的高质量“指令 - 回答”对(Prompt-Response pairs)。
*   **过程:** 使用少量高质量数据对预训练模型进行微调,使其适应对话、问答、总结等特定任务格式。

#### 3.3 人类反馈强化学习(Reinforcement Learning from Human Feedback, RLHF)
*   **目标:** 对齐人类价值观,使模型更安全、更有用、更符合人类偏好。
*   **过程:**
    1.  **奖励模型训练(Reward Model Training):** 让人类对模型的不同回答进行排序,训练一个奖励模型来预测人类的偏好分数。
    2.  **强化学习优化:** 使用 PPO(Proximal Policy Optimization)等算法,根据奖励模型的反馈优化策略模型(即 LLM),最大化奖励得分。
*   **结果:** 减少有害内容,提高回答的有用性和安全性。

### 4. 关键特性与能力

1.  **上下文学习(In-Context Learning):** 模型无需更新权重,仅通过输入 Prompt 中包含的示例(Few-shot 或 Zero-shot),即可在推理时学习新任务。
2.  **长文本理解:** 随着上下文窗口(Context Window)的扩大(从几千 Token 到几十万甚至百万 Token),模型能处理整本书、长代码库或长视频字幕。
3.  **多模态能力:** 现代 LLM 通常结合视觉编码器(如 CLIP),成为多模态大模型(LMM),能“看”图并理解其中的内容。
4.  **思维链(Chain of Thought, CoT):** 通过引导模型输出推理步骤,显著提升其在数学、逻辑推理任务上的表现。

### 5. 技术挑战与局限性

尽管 LLM 表现强大,但在技术层面仍存在显著挑战:

*   **幻觉(Hallucination):** 模型可能自信地生成错误或不存在的信息。这是因为模型是在“预测概率”而非“检索事实”。
*   **上下文窗口限制:** 虽然窗口在扩大,但处理超长上下文时计算复杂度呈平方级增长($O(N^2)$),导致推理延迟和显存占用高。
*   **知识截止(Knowledge Cutoff):** 训练数据有截止时间,模型不知道训练后发生的新事件(除非通过外挂知识库 RAG)。
*   **可解释性差(Black Box):** 神经网络的内部决策过程难以被人类直观理解,存在“黑盒”风险。
*   **计算资源消耗:** 训练和部署大模型需要昂贵的 GPU/TPU 集群和巨大的电力消耗。
*   **偏见与安全问题:** 训练数据中的人类偏见可能被模型继承,导致生成歧视性或有害内容。

### 6. 典型应用场景

*   **智能助手:** 客服机器人、个人助理(如 Siri 的进化版)。
*   **内容创作:** 撰写文章、代码生成(如 GitHub Copilot)、营销文案、剧本创作。
*   **信息处理:** 文本摘要、翻译、情感分析、数据提取。
*   **代码工程:** 代码补全、Bug 修复、代码解释与转换。
*   **企业知识库(RAG):** 结合检索增强生成(Retrieval-Augmented Generation),让模型基于私有数据回答问题,减少幻觉。

### 7. 总结

大语言模型是人工智能从“判别式”向“生成式”转变的关键技术。它通过**参数规模**和**数据规模**的扩张,涌现出了复杂的认知和推理能力。

未来的技术演进方向包括:
*   **模型小型化(SLM):** 让模型在终端设备上运行。
*   **多模态深度融合:** 从纯文本向视频、3D、物理世界交互扩展。
*   **智能体(Agent):** 赋予模型规划、工具使用和自主行动的能力,使其不仅能“说话”,还能“做事”。

希望这个详细的技术解释能帮助你理解大语言模型的本质。如果有特定方面(如数学原理、具体模型对比)想深入了解,欢迎继续提问。

==================== 第 3 轮:不传 context(默认 user) ====================


**大语言模型(Large Language Model,简称 LLM)** 是一种基于深度学习的人工智能技术,它能够理解、生成和操纵人类语言。

简单来说,它是人工智能领域中的一种“超级文本生成器”,经过海量文本数据的训练,可以像人类一样进行对话、写作、翻译、编程和逻辑推理。

为了让你更全面地理解,我们可以从以下几个维度来拆解:

### 1. 为什么叫“大”?
“大”主要体现在两个方面:
*   **参数量巨大(Parameters):** 模型内部包含数以亿计甚至万亿计的参数(可以理解为模型内部的“知识记忆点”和“连接权重”)。参数越多,模型通常越能捕捉语言中细微的规律和复杂的逻辑。
*   **训练数据巨大(Training Data):** 模型是在互联网上抓取的海量文本数据(包括书籍、网页、代码、文章等)上进行训练的,数据量通常达到 TB 甚至 PB 级别。

### 2. 核心工作原理
大语言模型的核心任务通常是**预测下一个词(或 Token)**。
*   **输入:** 你给它一段文字(提示词/Prompt)。
*   **处理:** 模型根据它学到的语言规律和知识,计算这句话后面最可能出现的下一个字或词的概率。
*   **输出:** 它输出概率最高的词,然后把这个词加到输入里,再预测下一个,以此类推,直到生成完整的句子或文章。

目前主流的架构是 **Transformer**(例如 2017 年提出的 Attention 机制),这使得模型能够同时处理长文本并理解上下文关系。

### 3. 主要能力
大语言模型不仅仅是聊天机器人,它的功能非常广泛:
*   **内容创作:** 写文章、写诗、写邮件、写剧本。
*   **问答与知识检索:** 回答百科知识、解释概念(虽然它可能会“幻觉”)。
*   **代码生成与调试:** 编写 Python、Java 等代码,并解释代码逻辑。
*   **多语言翻译:** 在几十种语言之间进行流畅互译。
*   **逻辑推理与分析:** 总结长文档、提取关键信息、进行数学计算或逻辑推演。

### 4. 局限性与挑战
尽管很强大,大语言模型并不是完美的:
*   **幻觉(Hallucination):** 模型有时会一本正经地胡说八道,编造事实或数据。
*   **缺乏真正的理解:** 它本质上是基于概率的统计模型,并不像人类那样拥有真正的“意识”或“理解力”。
*   **偏见(Bias):** 如果训练数据中包含偏见,模型也会输出带有偏见的内容。
*   **知识截止:** 模型的知识通常只更新到训练数据截止的那一天,不知道最新的新闻(除非联网搜索)。
*   **隐私与安全:** 用户输入的数据可能被用于训练,需要注意隐私保护。

### 5. 常见的代表模型
*   **GPT 系列:** 由 OpenAI 开发(如 GPT-3.5, GPT-4)。
*   **Claude 系列:** 由 Anthropic 开发。
*   **Llama 系列:** 由 Meta 开发,开源社区非常活跃。
*   **通义千问(Qwen):** 由阿里巴巴开发,我是其中的一员。
*   **文心一言、Kimi、Gemini 等:** 其他大厂或机构开发的模型。

### 总结
大语言模型是人工智能发展史上的一个重要里程碑,它标志着 AI 从“判别式”(如识别图片里有没有猫)向“生成式”(如生成一张猫的图片或关于猫的故事)的转变。它正在深刻改变我们获取信息、学习和工作的的方式。

运行结果解读

  • 第 1 轮(beginner):全程零行话——“文字接龙"猜下一个字、“超级博学的学生”,还主动分点说明"它能帮你做什么”——正是提示词里"简单地解释概念,避免行话"的效果;
  • 第 2 轮(expert):同一个问题,回答变成了带 Scaling Law、Decoder-only、RLHF/PPO 的技术长文——"提供详细的技术回应"生效;
  • 第 3 轮(默认 user):没传 context,走兜底分支拿到基础提示词,回答风格居中(有结构但没堆术语);
  • 三轮对照说明:问题一个字没变,变的只是 context 里的角色——改写系统提示词的中间件在每次调用前"换工牌",这就是动态提示词的全部意义。

踩坑实录(本节实测遇到):第 3 轮不传 context 时直接报错 AttributeError: 'NoneType' object has no attribute 'get'——原因是不传 context 时 request.runtime.contextNone,不是空字典.get("键", 默认值) 的默认值只能防"字典里没这个键",防不了"字典本身不存在"。修正写法:(request.runtime.context or {}).get("user_role", "user")——or {} 的意思是"左边如果是 None 就用空字典顶上",让后面的 .get 兜底真正生效。这跟「中间件与记忆」里的旧签名坑是同一类教训:示例代码在当前版本上跑一遍,报错原文就是最好的学习材料

多模态消息:content 管理图像/音频/文件

先回答"对不对":对——多模态输入就是把 content 当成块列表用:文字是 text 块、图片是 image 块(url 或 base64 两种来源)、PDF 是 file 块、音频是 audio 块,全部并排放在同一条消息里。图例的三组写法(图片 url/base64、PDF url/base64)就是这个标准的体现。

比喻:content 是餐盘,块是盘里的菜——文字、图片、音频、文件各占一格;模型是食客,能吃几样看它的能力("支持多模态"的模型才吃图片,全模态模型才吃音频)。LangChain 只负责把餐盘端出去,接不接收是后厨(模型服务)说了算。

这个设计的作用(为什么值得统一)

  1. 一个管道通吃:统一成消息之前,图像要走视觉接口、音频要走语音接口、文档要走文档接口,各写各的调用代码;统一成消息之后,多轮对话、记忆(checkpointer)、Agent、工具调用那一整套机制对图片同样生效——发过图的历史会进记忆,Agent 能在对话里看图调工具,不用为多模态另起炉灶;
  2. 多模态 RAG 的地基:以后的"对图表提问"“扫描件问答”,本质都是把图/文件作为块塞进消息,和后面要学的 RAG 检索接在一起;
  3. 职责清晰:指令(text 块)和素材(image/file 块)分开放,程序按 block["type"] 就能拆开处理。

两种图片来源的取舍url 来源是服务端自己去下载(要求那个地址公网可达、不防热链);base64 来源是本地数据直接编码进请求(不依赖图床,但消息体会变大)。

"""
多模态消息 —— 图像/音频/PDF 都用 content 标准块描述:图像可走消息进模型,其余各有边界(实测)
"""
import os                          # Python 自带:路径拼接、读环境变量
import base64                      # Python 自带:把二进制转成 base64 文本(图片/音频进消息的前提)
import io                          # Python 自带:内存中的"临时文件",图片先存这里再编码
import json                        # Python 自带:拼/解析 JSON 请求体
import uuid                        # Python 自带:生成随机串(multipart 分隔线用)
import urllib.request              # Python 自带:直接调 HTTP 接口(TTS/ASR 不走 chat,模型包装器管不到)
from dotenv import load_dotenv     # 读取 .env 文件(密钥外置)
from langchain_openai import ChatOpenAI        # 模型包装器(视觉模型也用同一个类接)
from langchain_core.messages import HumanMessage   # 用户消息类(content_blocks 构造多模态消息)
from PIL import Image, ImageDraw   # PIL(Pillow):本地画一张测试图,免去找图/下载

load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__fi
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值