This commit is contained in:
kazusa 2026-01-14 09:58:47 +08:00
parent 04ae209fdc
commit 147eb780f4
14 changed files with 1584 additions and 0 deletions

193
LANGCHAIN_LEARN.md Normal file
View File

@ -0,0 +1,193 @@
# LangChain + Ollama 零基础学习指南
> 本地运行、完全免费的 LLM 应用开发学习
## 环境准备
### 1. 安装 Ollama
```bash
# macOS
brew install ollama
# 或下载: https://ollama.ai/download
```
### 2. 下载模型
```bash
# 启动服务
ollama serve
# 下载推荐模型(新终端)
ollama pull qwen2.5:7b # 主力模型,中文强
ollama pull bge-m3 # Embedding 模型,中文效果好
# 可选模型
ollama pull llama3.2:3b # 轻量级
ollama pull codellama:7b # 代码专用
```
### 3. 安装 Python 依赖
```bash
cd /Users/kazusa/llm/mini_lm
python -m venv venv
source venv/bin/activate
pip install langchain langchain-ollama langchain-community faiss-cpu python-dotenv pydantic
```
---
## 学习路线图
```
📅 Week 1: 基础篇
├── Day 1: LLM 调用 + Prompt Template + Output Parser + Chain 基础
├── Day 2: LCEL 表达式语言深入
├── Day 3: Memory记忆管理
└── Day 4: 综合练习
📅 Week 2: 核心篇
├── Day 5: Tool工具定义与调用
├── Day 6: RAG 基础(向量化 + 检索)
├── Day 7: RAG 进阶(文档加载 + 分割策略)
└── Day 8: 综合练习
📅 Week 3: Agent 篇 ⭐
├── Day 9: Agent 基础(工具调用 Agent
├── Day 10: ReAct Agent推理+行动循环)
├── Day 11: 自定义 Agent
└── Day 12: 综合练习
📅 Week 4: 实战篇
├── Day 13: 项目1 - 智能文档问答助手
├── Day 14: 项目2 - 代码审查助手
└── Day 15: 部署与优化
```
---
## 课程目录
### Day 1: 基础入门
| 文件 | 主题 | 核心概念 |
|------|------|----------|
| `src/day1/01_hello_llm.py` | 第一次调用 | ChatOllama、invoke |
| `src/day1/02_prompt_template.py` | 提示词模板 | ChatPromptTemplate、变量替换 |
| `src/day1/03_output_parser.py` | 输出解析 | StrOutputParser、JsonOutputParser |
| `src/day1/04_chain_basics.py` | 链式调用 | LCEL、管道符 `|` |
| `src/day1/05_practice.py` | 综合练习 | 组合运用 |
### Day 2: LCEL 深入 ✅ 今日学习
| 文件 | 主题 | 核心概念 |
|------|------|----------|
| `src/day2/01_lcel_basics.py` | LCEL 语法 | RunnableSequence |
| `src/day2/02_parallel.py` | 并行执行 | RunnableParallel |
| `src/day2/03_passthrough.py` | 数据透传 | RunnablePassthrough |
| `src/day2/04_branch.py` | 条件分支 | RunnableBranch |
### Day 3: Memory 记忆
| 文件 | 主题 | 核心概念 |
|------|------|----------|
| `src/day3/01_chat_history.py` | 对话历史 | InMemoryChatMessageHistory |
| `src/day3/02_memory_chain.py` | 记忆链 | RunnableWithMessageHistory |
| `src/day3/03_window_memory.py` | 窗口记忆 | 限制历史长度 |
### Day 5: Tool 工具
| 文件 | 主题 | 核心概念 |
|------|------|----------|
| `src/day5/01_tool_basics.py` | 工具定义 | @tool 装饰器 |
| `src/day5/02_bind_tools.py` | 绑定工具 | bind_tools |
| `src/day5/03_tool_execution.py` | 执行工具 | ToolMessage |
### Day 6-7: RAG 检索增强
| 文件 | 主题 | 核心概念 |
|------|------|----------|
| `src/day6/01_embeddings.py` | 向量化 | OllamaEmbeddings |
| `src/day6/02_vectorstore.py` | 向量存储 | FAISS |
| `src/day6/03_retriever.py` | 检索器 | as_retriever |
| `src/day6/04_rag_chain.py` | RAG 链 | 完整 RAG 流程 |
| `src/day7/01_doc_loaders.py` | 文档加载 | TextLoader、DirectoryLoader |
| `src/day7/02_text_splitter.py` | 文本分割 | RecursiveCharacterTextSplitter |
### Day 9-11: Agent 智能体 ⭐
| 文件 | 主题 | 核心概念 |
|------|------|----------|
| `src/day9/01_agent_basics.py` | Agent 基础 | create_tool_calling_agent |
| `src/day9/02_agent_executor.py` | 执行器 | AgentExecutor |
| `src/day10/01_react_agent.py` | ReAct | 推理-行动循环 |
| `src/day11/01_custom_agent.py` | 自定义 Agent | 完全控制 Agent 行为 |
### Day 13-14: 实战项目
| 文件 | 项目 | 功能 |
|------|------|------|
| `src/projects/doc_qa_assistant.py` | 文档问答助手 | RAG + Memory |
| `src/projects/code_reviewer.py` | 代码审查助手 | Tool + Agent |
---
## 核心概念速查
### LCELLangChain Expression Language
```python
# 管道符 | 连接组件
chain = prompt | llm | parser
# 等价于
chain = RunnableSequence(prompt, llm, parser)
```
### 组件类型
| 组件 | 输入 | 输出 | 作用 |
|------|------|------|------|
| Prompt | dict | Messages | 构建提示词 |
| LLM | Messages | AIMessage | 生成回复 |
| Parser | AIMessage | str/dict | 解析输出 |
| Retriever | str | Documents | 检索文档 |
| Tool | dict | str | 执行操作 |
---
## 运行说明
```bash
# 确保 Ollama 服务运行
ollama serve
# 激活虚拟环境
source venv/bin/activate
# 运行课程文件
python src/day1/01_hello_llm.py
```
---
## 模型推荐
| 场景 | 模型 | 命令 |
|------|------|------|
| 日常学习 | qwen2.5:7b | `ollama pull qwen2.5:7b` |
| 低配机器 | qwen2.5:3b | `ollama pull qwen2.5:3b` |
| RAG Embedding | bge-m3 | `ollama pull bge-m3` |
| 代码任务 | codellama:7b | `ollama pull codellama:7b` |
---
## 学习建议
1. **动手为主**:每个文件都要运行,修改参数观察变化
2. **循序渐进**:按 Day 顺序学习,不要跳跃
3. **做好笔记**:在代码注释中记录你的理解
4. **综合练习**:每个阶段结束后完成 practice 文件

23
README.md Normal file
View File

@ -0,0 +1,23 @@
从零训练一个迷你语言模型
第 1 天:字符级 RNNGRU
环境要求
- Python 3.10+
- PyTorchApple Silicon 支持 MPS
安装与准备
- python -m venv .venv
- source .venv/bin/activate
- pip install torch
运行
- python day1_char_rnn.py
你应该看到
- 每 100 步输出一次 loss
- 每 200 步输出一次采样文本
说明
- 这是一个极小的数据集和模型,只为验证完整训练流程。
- 下一步我们会把硬编码文本替换为文件,并加入评估指标。

156
day1_char_rnn.py Normal file
View File

@ -0,0 +1,156 @@
import math
import random
import time
from dataclasses import dataclass
import torch
import torch.nn as nn
import torch.nn.functional as F
TEXT = (
"起初只有一个简单的故事。"
"它不长,但足够教会模型去猜下一个字符。"
"我们会在这段文字上训练一个很小的语言模型,并观察它学习。"
"实践胜过空谈,所以先从小处开始,不断迭代。"
"很快我们会用更大的数据替换它。"
"现在先保持小而清晰。"
)
@dataclass
class Config:
# 序列长度(模型一次看到的字符数)
seq_len: int = 64
# 批大小
batch_size: int = 32
# 字符嵌入维度
embed_dim: int = 64
# GRU 隐藏层维度
hidden_dim: int = 128
# GRU 层数
num_layers: int = 1
# 学习率
lr: float = 2e-3
# 训练步数
steps: int = 800
# 采样频率
sample_every: int = 200
# 采样长度
sample_len: int = 200
# 采样温度(越高越随机)
temperature: float = 0.9
class CharRNN(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers):
super().__init__()
# 字符索引 -> 向量
self.embed = nn.Embedding(vocab_size, embed_dim)
# GRU 用于处理序列上下文
self.rnn = nn.GRU(
embed_dim,
hidden_dim,
num_layers=num_layers,
batch_first=True,
)
# 隐藏状态 -> 下一个字符的 logits
self.fc = nn.Linear(hidden_dim, vocab_size)
def forward(self, x, h=None):
# x: (batch, seq_len)
x = self.embed(x)
# out: (batch, seq_len, hidden_dim)
out, h = self.rnn(x, h)
# logits: (batch, seq_len, vocab_size)
logits = self.fc(out)
return logits, h
def build_vocab(text):
# 构建字符表与双向映射
chars = sorted(set(text))
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for ch, i in stoi.items()}
return chars, stoi, itos
def encode(text, stoi):
# 文本 -> 索引序列
return torch.tensor([stoi[ch] for ch in text], dtype=torch.long)
def get_batch(data, cfg):
# 随机抽取若干段序列,生成输入 x 和目标 yy 是 x 向后移一位)
max_start = len(data) - cfg.seq_len - 1
starts = torch.randint(0, max_start, (cfg.batch_size,))
x = torch.stack([data[s : s + cfg.seq_len] for s in starts])
y = torch.stack([data[s + 1 : s + cfg.seq_len + 1] for s in starts])
return x, y
def sample(model, stoi, itos, device, cfg, start="起初"):
# 从给定起始串开始,按概率逐字符采样
model.eval()
# 如果起始字符不在词表中,使用文本开头的字符兜底
safe_start = "".join(ch for ch in start if ch in stoi)
if not safe_start:
safe_start = TEXT[:1]
idx = torch.tensor([stoi[ch] for ch in safe_start], dtype=torch.long, device=device)
idx = idx.unsqueeze(0)
h = None
with torch.no_grad():
for _ in range(cfg.sample_len):
logits, h = model(idx, h)
# 只取最后一个位置的预测
logits = logits[:, -1, :] / cfg.temperature
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, next_id], dim=1)
out = "".join(itos[i] for i in idx[0].tolist())
return out
def main():
cfg = Config()
# 字符表与编码
chars, stoi, itos = build_vocab(TEXT)
data = encode(TEXT, stoi)
# Apple Silicon 上优先用 MPS
device = "mps" if torch.backends.mps.is_available() else "cpu"
model = CharRNN(len(chars), cfg.embed_dim, cfg.hidden_dim, cfg.num_layers).to(device)
opt = torch.optim.AdamW(model.parameters(), lr=cfg.lr)
start_time = time.time()
model.train()
for step in range(1, cfg.steps + 1):
x, y = get_batch(data, cfg)
x = x.to(device)
y = y.to(device)
logits, _ = model(x)
# 交叉熵:预测下一个字符
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1))
opt.zero_grad()
loss.backward()
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
if step % 100 == 0:
elapsed = time.time() - start_time
speed = step / max(elapsed, 1e-6)
print(f"step {step} | loss {loss.item():.4f} | {speed:.1f} steps/s")
if step % cfg.sample_every == 0:
print("-" * 60)
print(sample(model, stoi, itos, device, cfg))
print("-" * 60)
if __name__ == "__main__":
# 固定随机种子,便于复现
random.seed(42)
torch.manual_seed(42)
main()

22
requirements.txt Normal file
View File

@ -0,0 +1,22 @@
# LangChain 核心
langchain>=0.3.0
langchain-core>=0.3.0
langchain-community>=0.3.0
# Ollama 支持
langchain-ollama>=0.2.0
# OpenAI 支持(可选)
langchain-openai>=0.2.0
# 向量存储
faiss-cpu>=1.8.0
# 数据验证
pydantic>=2.0.0
# 环境变量
python-dotenv>=1.0.0
# 其他工具
tiktoken>=0.7.0

85
src/day1/01_hello_llm.py Normal file
View File

@ -0,0 +1,85 @@
"""
Day 1 - 课程 1: 第一次调用 LLM
=============================
学习目标
- 理解 ChatOllama 的基本用法
- 掌握 invoke() 方法
- 了解消息类型SystemMessage, HumanMessage, AIMessage
核心概念
- ChatOllama: LangChain Ollama 的封装
- invoke(): 同步调用方法
- Messages: LangChain 的消息抽象
"""
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_ollama import ChatOllama
# ==================== 1. 创建模型 ====================
# model: 指定 Ollama 中的模型名ollama list 查看已下载模型)
# temperature: 0-1越低越确定性越高越随机
# base_url: Ollama 服务地址,默认 http://localhost:11434
llm = ChatOllama(
model="qwen2.5:7b",
temperature=0,
# base_url="http://localhost:11434" # 默认值,一般不用改
)
print("=" * 50)
print("1. 最简单的调用 - 直接传字符串")
print("=" * 50)
response = llm.invoke("你好,用一句话介绍你自己")
print(f"类型: {type(response)}")
print(f"内容: {response.content}")
print()
# ==================== 2. 使用消息对象 ====================
print("=" * 50)
print("2. 使用消息对象 - 带系统提示")
print("=" * 50)
messages = [
SystemMessage(content="你是一个Python专家回答要简洁不超过50字"),
HumanMessage(content="什么是列表推导式?")
]
response = llm.invoke(messages)
print(f"回答: {response.content}")
print()
# ==================== 3. 多轮对话模拟 ====================
print("=" * 50)
print("3. 多轮对话模拟")
print("=" * 50)
# 手动构建对话历史
conversation = [
SystemMessage(content="你是一个友好的助手"),
HumanMessage(content="我叫小明"),
AIMessage(content="你好小明!很高兴认识你!"), # 模拟之前的回复
HumanMessage(content="我叫什么名字?") # 当前问题
]
response = llm.invoke(conversation)
print(f"回答: {response.content}")
print()
# ==================== 4. 流式输出 ====================
print("=" * 50)
print("4. 流式输出 - 逐字显示")
print("=" * 50)
print("回答: ", end="", flush=True)
for chunk in llm.stream("用3句话介绍机器学习"):
print(chunk.content, end="", flush=True)
print("\n")
# ==================== 练习 ====================
"""
练习 1: 修改 temperature 0.9多次运行观察输出变化
练习 2: 尝试不同的系统提示"你是一个诗人""你是一个程序员"
练习 3: 构建一个 5 轮的对话历史测试模型是否能记住上下文
"""

View File

@ -0,0 +1,162 @@
"""
Day 1 - 课程 2: Prompt Template提示词模板
==========================================
学习目标
- 理解为什么需要 Prompt Template
- 掌握 ChatPromptTemplate 的创建方式
- 学会使用变量占位符
核心概念
- PromptTemplate: 字符串模板
- ChatPromptTemplate: 聊天消息模板
- invoke(): 填充变量生成最终 prompt
为什么需要模板
- 复用同一模板可以用不同变量
- 维护集中管理提示词
- 类型安全明确需要哪些变量
"""
from langchain_core.messages import HumanMessage
from langchain_core.prompts import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
MessagesPlaceholder,
PromptTemplate,
SystemMessagePromptTemplate,
)
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
# ==================== 1. 基础字符串模板 ====================
print("=" * 50)
print("1. 基础字符串模板 - PromptTemplate")
print("=" * 50)
# 使用 {变量名} 作为占位符
template = PromptTemplate.from_template(
"请用{language}语言写一个{function_name}函数,功能是{description}"
)
# 查看模板信息
print(f"模板变量: {template.input_variables}")
# 填充变量
prompt = template.invoke({
"language": "Python",
"function_name": "fibonacci",
"description": "计算斐波那契数列第n项"
})
print(f"生成的prompt: {prompt.text}")
print()
# ==================== 2. 聊天消息模板 ====================
print("=" * 50)
print("2. 聊天消息模板 - ChatPromptTemplate")
print("=" * 50)
# 方式1from_messages - 最常用
chat_template = ChatPromptTemplate.from_messages([
("system", "你是一个{role},回答风格要{style}"),
("human", "{question}")
])
messages = chat_template.invoke({
"role": "幽默的程序员",
"style": "轻松有趣",
"question": "什么是递归?"
})
print(f"生成的消息: {messages}")
print()
# 调用 LLM
response = llm.invoke(messages)
print(f"回答: {response.content}")
print()
# ==================== 3. 使用消息模板类 ====================
print("=" * 50)
print("3. 使用消息模板类(更精细的控制)")
print("=" * 50)
# 分别创建系统消息和用户消息模板
system_template = SystemMessagePromptTemplate.from_template(
"你是{company}公司的客服,公司主营{business}"
)
human_template = HumanMessagePromptTemplate.from_template(
"客户问题:{question}"
)
chat_template = ChatPromptTemplate.from_messages([
system_template,
human_template
])
messages = chat_template.invoke({
"company": "美味餐厅",
"business": "中式快餐",
"question": "你们营业到几点?"
})
response = llm.invoke(messages)
print(f"客服回答: {response.content}")
print()
# ==================== 4. 多变量复杂模板 ====================
print("=" * 50)
print("4. 代码审查模板示例")
print("=" * 50)
code_review_template = ChatPromptTemplate.from_messages([
("system", """你是一个资深{language}开发者,负责代码审查。
审查时请关注
1. 代码风格和可读性
2. 潜在的 bug
3. 性能问题
4. 安全漏洞
请用{output_format}格式输出审查结果"""),
("human", "请审查以下代码:\n```{language}\n{code}\n```")
])
messages = code_review_template.invoke({
"language": "Python",
"output_format": "Markdown",
"code": """def get_user(id):
sql = f"SELECT * FROM users WHERE id = {id}"
return db.execute(sql)"""
})
response = llm.invoke(messages)
print(f"审查结果:\n{response.content}")
print()
# ==================== 5. 部分变量填充 ====================
print("=" * 50)
print("5. 部分变量填充 - partial")
print("=" * 50)
# 创建通用模板
template = ChatPromptTemplate.from_messages([
("system", "你是{company}的AI助手"),
("human", "{question}")
])
# 预填充公司名,创建专用模板
company_template = template.partial(company="LangChain学习小组")
# 使用时只需要提供 question
messages = company_template.invoke({"question": "你们是做什么的?"})
response = llm.invoke(messages)
print(f"回答: {response.content}")
print()
# ==================== 练习 ====================
"""
练习 1: 创建一个"翻译助手"模板支持指定源语言目标语言和待翻译文本
练习 2: 创建一个"面试官"模板可以指定面试岗位和难度级别
练习 3: 创建一个"故事生成器"模板支持指定主角背景和风格
"""

View File

@ -0,0 +1,198 @@
"""
Day 1 - 课程 3: Output Parser输出解析器
=========================================
学习目标
- 理解为什么需要输出解析
- 掌握常用解析器StrOutputParserJsonOutputParser
- 学会使用 Pydantic 定义结构化输出
核心概念
- Parser: LLM 的文本输出转换为程序可用的格式
- format_instructions: 告诉 LLM 期望的输出格式
- Pydantic: Python 的数据验证库
为什么需要解析器
- LLM 输出是字符串程序需要结构化数据
- 自动提取关键信息
- 类型验证和错误处理
"""
import json
from typing import List
from langchain_core.output_parsers import JsonOutputParser, StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama
from pydantic import BaseModel, Field
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
# ==================== 1. StrOutputParser ====================
print("=" * 50)
print("1. StrOutputParser - 最简单的解析器")
print("=" * 50)
# 默认 LLM 返回的是 AIMessage 对象
response = llm.invoke("说一个笑话")
print(f"原始类型: {type(response)}")
print(f"原始内容: {response}")
print()
# 使用 StrOutputParser 提取纯文本
parser = StrOutputParser()
text = parser.invoke(response)
print(f"解析后类型: {type(text)}")
print(f"解析后内容: {text}")
print()
# ==================== 2. 在链中使用解析器 ====================
print("=" * 50)
print("2. 在链中使用解析器")
print("=" * 50)
prompt = ChatPromptTemplate.from_template("用一句话解释{concept}")
# 不使用解析器
chain_without_parser = prompt | llm
result1 = chain_without_parser.invoke({"concept": "人工智能"})
print(f"不使用解析器: {type(result1)} -> {result1}")
print()
# 使用解析器
chain_with_parser = prompt | llm | StrOutputParser()
result2 = chain_with_parser.invoke({"concept": "人工智能"})
print(f"使用解析器: {type(result2)} -> {result2}")
print()
# ==================== 3. JsonOutputParser ====================
print("=" * 50)
print("3. JsonOutputParser - JSON 格式输出")
print("=" * 50)
# 定义期望的数据结构
class BookInfo(BaseModel):
"""书籍信息"""
title: str = Field(description="书名")
author: str = Field(description="作者")
year: int = Field(description="出版年份")
summary: str = Field(description="一句话简介")
# 创建 JSON 解析器
json_parser = JsonOutputParser(pydantic_object=BookInfo)
# 获取格式说明(会告诉 LLM 期望什么格式)
format_instructions = json_parser.get_format_instructions()
print(f"格式说明:\n{format_instructions}\n")
# 构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个图书馆助手。请严格按照指定格式输出。\n{format_instructions}"),
("human", "介绍一下《{book_name}》这本书")
])
# 构建链
chain = prompt | llm | json_parser
# 调用(注意:需要传入 format_instructions
try:
result = chain.invoke({
"book_name": "三体",
"format_instructions": format_instructions
})
print(f"解析结果类型: {type(result)}")
print(f"书名: {result['title']}")
print(f"作者: {result['author']}")
print(f"年份: {result['year']}")
print(f"简介: {result['summary']}")
except Exception as e:
print(f"解析失败: {e}")
print()
# ==================== 4. 复杂结构化输出 ====================
print("=" * 50)
print("4. 复杂结构化输出 - 代码审查")
print("=" * 50)
class CodeIssue(BaseModel):
"""代码问题"""
line: int = Field(description="问题所在行号")
severity: str = Field(description="严重程度: low/medium/high")
description: str = Field(description="问题描述")
class CodeReview(BaseModel):
"""代码审查结果"""
score: int = Field(description="代码质量评分 1-10")
issues: List[CodeIssue] = Field(description="发现的问题列表")
suggestions: List[str] = Field(description="改进建议列表")
summary: str = Field(description="审查总结")
review_parser = JsonOutputParser(pydantic_object=CodeReview)
schema = CodeReview.model_json_schema()
print(json.dumps(schema, indent=2, ensure_ascii=False))
review_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个资深代码审查专家。
请仔细审查代码并按照指定格式输出结果
{format_instructions}"""),
("human", "审查以下Python代码\n```python\n{code}\n```")
])
review_chain = review_prompt | llm | review_parser
code_to_review = """
def calculate_total(items):
total = 0
for i in range(len(items)):
total = total + items[i]['price'] * items[i]['quantity']
return total
"""
try:
review_result = review_chain.invoke({
"code": code_to_review,
"format_instructions": review_parser.get_format_instructions()
})
print(f"评分: {review_result['score']}/10")
print(f"\n发现的问题:")
for issue in review_result['issues']:
print(f" - [行 {issue['line']}][{issue['severity']}] {issue['description']}")
print(f"\n改进建议:")
for suggestion in review_result['suggestions']:
print(f" - {suggestion}")
print(f"\n总结: {review_result['summary']}")
except Exception as e:
print(f"解析失败: {e}")
print()
# ==================== 5. 简化写法with_structured_output ====================
print("=" * 50)
print("5. 简化写法 - with_structured_output")
print("=" * 50)
# 注意:此功能需要模型支持结构化输出
# Ollama 的某些模型可能不完全支持,如果失败请使用上面的方式
class SimpleAnswer(BaseModel):
"""简单回答"""
answer: str = Field(description="答案")
confidence: float = Field(description="置信度 0-1")
try:
# 让 LLM 直接输出结构化数据
structured_llm = llm.with_structured_output(SimpleAnswer)
result = structured_llm.invoke("法国的首都是哪里?")
print(f"答案: {result.answer}")
print(f"置信度: {result.confidence}")
except Exception as e:
print(f"with_structured_output 不支持,请使用 JsonOutputParser: {e}")
# ==================== 练习 ====================
"""
练习 1: 创建一个解析器提取新闻文章的标题摘要关键词
练习 2: 创建一个解析器解析餐厅评论的评分优点缺点
练习 3: 修改代码审查解析器添加"是否建议合并"字段
"""

193
src/day1/04_chain_basics.py Normal file
View File

@ -0,0 +1,193 @@
"""
Day 1 - 课程 4: Chain 基础链式调用
====================================
学习目标
- 理解 LCELLangChain Expression Language
- 掌握管道符 | 的用法
- 学会组合多个组件构建工作流
核心概念
- LCEL: LangChain 的表达式语言
- Runnable: 所有可链接组件的基类
- 管道符 |: 连接组件前一个输出作为后一个输入
LCEL 优势
- 简洁 | 代替复杂的嵌套
- 统一所有组件遵循相同接口
- 强大支持并行分支错误处理
"""
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
# ==================== 1. 基础链 ====================
print("=" * 50)
print("1. 基础链: prompt | llm | parser")
print("=" * 50)
# 最基础的链:提示词 -> 模型 -> 解析
prompt = ChatPromptTemplate.from_template("用一句话解释{concept}")
# 方式1使用管道符
chain = prompt | llm | parser
result = chain.invoke({"concept": "量子计算"})
print(f"结果: {result}")
print()
# ==================== 2. 查看链的结构 ====================
print("=" * 50)
print("2. 查看链的结构")
print("=" * 50)
print(f"链的类型: {type(chain)}")
print(f"链的步骤: {chain}")
# 可以获取链的 schema
print(f"\n输入 Schema: {chain.input_schema.model_json_schema()}")
print()
# ==================== 3. 多步骤链 ====================
print("=" * 50)
print("3. 多步骤链: 翻译 -> 总结")
print("=" * 50)
# 第一步:翻译成英文
translate_prompt = ChatPromptTemplate.from_template(
"将以下中文翻译成英文,只输出翻译结果:\n{text}"
)
translate_chain = translate_prompt | llm | parser
# 第二步:用英文总结
summarize_prompt = ChatPromptTemplate.from_template(
"Summarize the following text in one sentence:\n{english_text}"
)
summarize_chain = summarize_prompt | llm | parser
# 手动串联
chinese_text = "人工智能正在改变我们的生活方式从智能手机到自动驾驶汽车AI无处不在。"
english = translate_chain.invoke({"text": chinese_text})
print(f"翻译结果: {english}")
summary = summarize_chain.invoke({"english_text": english})
print(f"英文总结: {summary}")
print()
# ==================== 4. RunnableLambda - 自定义函数 ====================
print("=" * 50)
print("4. RunnableLambda - 在链中插入自定义逻辑")
print("=" * 50)
# 定义处理函数
def add_prefix(text: str) -> str:
return f"[AI回答] {text}"
def word_count(text: str) -> dict:
return {
"text": text,
"word_count": len(text),
"char_count": len(text.replace(" ", "")),
}
# 创建 Runnable
add_prefix_runnable = RunnableLambda(add_prefix)
word_count_runnable = RunnableLambda(word_count)
# 组合链
chain_with_lambda = (
ChatPromptTemplate.from_template("用20字以内回答{question}")
| llm
| parser
| add_prefix_runnable
| word_count_runnable
)
result = chain_with_lambda.invoke({"question": "什么是Python"})
print(f"结果: {result}")
print()
# ==================== 5. RunnablePassthrough - 数据透传 ====================
print("=" * 50)
print("5. RunnablePassthrough - 保留原始输入")
print("=" * 50)
# 场景:既要 LLM 的输出,也要保留原始问题
from langchain_core.runnables import RunnableParallel
chain_with_passthrough = RunnableParallel(
answer=ChatPromptTemplate.from_template("回答:{question}") | llm | parser,
original_question=RunnablePassthrough(), # 透传原始输入
)
result = chain_with_passthrough.invoke({"question": "天空为什么是蓝色的?"})
print(f"原始问题: {result['original_question']}")
print(f"AI回答: {result['answer']}")
print()
# ==================== 6. 链的批量处理 ====================
print("=" * 50)
print("6. 批量处理 - batch")
print("=" * 50)
simple_chain = (
ChatPromptTemplate.from_template("用一个词描述{thing}的特点") | llm | parser
)
# 批量处理多个输入
inputs = [{"thing": "太阳"}, {"thing": "月亮"}, {"thing": "星星"}]
results = simple_chain.batch(inputs)
for inp, res in zip(inputs, results):
print(f"{inp['thing']}: {res}")
print()
# ==================== 7. 完整示例:问答增强链 ====================
print("=" * 50)
print("7. 完整示例:问答增强链")
print("=" * 50)
def enhance_question(question: str) -> str:
"""增强问题,添加上下文要求"""
return f"{question}\n\n请提供详细解释,并举一个例子。"
def format_response(response: str) -> str:
"""格式化响应"""
lines = response.strip().split("\n")
formatted = []
for i, line in enumerate(lines, 1):
if line.strip():
formatted.append(f" {line}")
return "\n".join(formatted)
qa_chain = (
RunnableLambda(lambda x: {"question": enhance_question(x["question"])})
| ChatPromptTemplate.from_template("{question}")
| llm
| parser
| RunnableLambda(format_response)
)
result = qa_chain.invoke({"question": "什么是装饰器?"})
print(f"回答:\n{result}")
# ==================== 练习 ====================
"""
练习 1: 创建一个"中文 -> 英文 -> 日文"的翻译链
练习 2: 创建一个链先生成代码然后自动添加注释
练习 3: 创建一个链输入关键词输出包含标题正文标签的文章结构
"""

54
src/day1/04_exec.py Normal file
View File

@ -0,0 +1,54 @@
"""
练习 1: 创建一个"中文 -> 英文 -> 日文"的翻译链
练习 2: 创建一个链先生成代码然后自动添加注释
练习 3: 创建一个链输入关键词输出包含标题正文标签的文章结构
"""
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
print("=" * 50)
print('练习 1: 创建一个"中文 -> 英文 -> 日文"的翻译链')
print("=" * 50)
en_prompt = ChatPromptTemplate.from_template("将下列这段话翻译成英文:{text}")
jp_prompt = ChatPromptTemplate.from_template("将下列这段话翻译成日文:{text}")
def package_translate(text: str) -> dict:
return {"text": text}
def print_answer(ans: str):
print(f"llm回答:{ans}")
return ans
package_step = RunnableLambda[str, dict](package_translate)
print_step = RunnableLambda[str, str](print_answer)
translate_chain = (
package_step
| en_prompt
| llm
| parser
| print_step
| package_step
| jp_prompt
| llm
| parser
| print_step
)
result = translate_chain.invoke("你好啊")
print(f"回答:\n{result}")
print("=" * 50)
print('练习 2: 创建一个链,先生成代码,然后自动添加注释')
print("=" * 50)
ChatPromptTemplate.from_template("请用Python帮我实现:{text}")

204
src/day1/05_practice.py Normal file
View File

@ -0,0 +1,204 @@
"""
Day 1 - 课程 5: 综合练习
======================
今日知识点回顾
1. ChatOllama - 调用本地 LLM
2. ChatPromptTemplate - 构建提示词模板
3. OutputParser - 解析 LLM 输出
4. Chain (LCEL) - 链式组合组件
本练习将综合运用以上知识构建一个实用的小工具
"""
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.runnables import RunnableLambda, RunnableParallel
from pydantic import BaseModel, Field
from typing import List
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
# ==================== 练习 1: 多语言翻译助手 ====================
print("=" * 60)
print("练习 1: 多语言翻译助手")
print("=" * 60)
class TranslationResult(BaseModel):
"""翻译结果"""
original: str = Field(description="原文")
translated: str = Field(description="译文")
source_lang: str = Field(description="源语言")
target_lang: str = Field(description="目标语言")
translation_parser = JsonOutputParser(pydantic_object=TranslationResult)
translation_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个专业翻译。将文本从{source_lang}翻译成{target_lang}
{format_instructions}"""),
("human", "翻译:{text}")
])
translation_chain = translation_prompt | llm | translation_parser
# 测试
try:
result = translation_chain.invoke({
"source_lang": "中文",
"target_lang": "英文",
"text": "学习永无止境",
"format_instructions": translation_parser.get_format_instructions()
})
print(f"原文 ({result['source_lang']}): {result['original']}")
print(f"译文 ({result['target_lang']}): {result['translated']}")
except Exception as e:
print(f"翻译失败: {e}")
print()
# ==================== 练习 2: 智能摘要生成器 ====================
print("=" * 60)
print("练习 2: 智能摘要生成器")
print("=" * 60)
class ArticleSummary(BaseModel):
"""文章摘要"""
title: str = Field(description="提取或生成的标题")
summary: str = Field(description="100字以内的摘要")
keywords: List[str] = Field(description="3-5个关键词")
sentiment: str = Field(description="情感倾向: positive/negative/neutral")
summary_parser = JsonOutputParser(pydantic_object=ArticleSummary)
summary_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个文本分析专家。分析给定文章并提取关键信息。
{format_instructions}"""),
("human", "分析以下文章:\n\n{article}")
])
summary_chain = summary_prompt | llm | summary_parser
article = """
人工智能AI正在以前所未有的速度改变我们的世界从智能语音助手到自动驾驶汽车
从医疗诊断到金融分析AI的应用已经渗透到生活的方方面面
特别是大语言模型的出现如GPT系列和开源的LLaMA系列让机器理解和生成人类语言
的能力达到了新的高度这些模型不仅能进行对话还能写代码创作文章分析数据
然而AI的快速发展也带来了一些担忧隐私问题就业影响算法偏见等议题需要
社会各界共同面对和解决未来如何在享受AI带来便利的同时确保其安全和公平
发展将是人类需要认真思考的问题
"""
try:
result = summary_chain.invoke({
"article": article,
"format_instructions": summary_parser.get_format_instructions()
})
print(f"标题: {result['title']}")
print(f"摘要: {result['summary']}")
print(f"关键词: {', '.join(result['keywords'])}")
print(f"情感: {result['sentiment']}")
except Exception as e:
print(f"分析失败: {e}")
print()
# ==================== 练习 3: 代码生成器 ====================
print("=" * 60)
print("练习 3: 代码生成器(带注释)")
print("=" * 60)
# 步骤1生成代码
code_gen_prompt = ChatPromptTemplate.from_template(
"""{language}语言实现以下功能,只输出代码,不要解释:
功能{requirement}"""
)
code_gen_chain = code_gen_prompt | llm | parser
# 步骤2添加注释
comment_prompt = ChatPromptTemplate.from_template(
"""为以下代码添加详细的中文注释,包括函数说明和关键步骤解释:
```
{code}
```
输出完整的带注释的代码"""
)
comment_chain = comment_prompt | llm | parser
# 组合:生成 -> 注释
def generate_and_comment(inputs: dict) -> str:
code = code_gen_chain.invoke(inputs)
commented = comment_chain.invoke({"code": code})
return commented
full_chain = RunnableLambda(generate_and_comment)
result = full_chain.invoke({
"language": "Python",
"requirement": "实现二分查找算法"
})
print(f"生成的代码:\n{result}")
print()
# ==================== 练习 4: 并行处理 - 多角度分析 ====================
print("=" * 60)
print("练习 4: 并行处理 - 多角度分析")
print("=" * 60)
# 同时从多个角度分析一个主题
topic_analysis_chain = RunnableParallel(
definition=ChatPromptTemplate.from_template("用一句话定义{topic}") | llm | parser,
advantages=ChatPromptTemplate.from_template("列出{topic}的3个主要优点") | llm | parser,
challenges=ChatPromptTemplate.from_template("列出{topic}面临的3个主要挑战") | llm | parser,
future=ChatPromptTemplate.from_template("预测{topic}未来5年的发展趋势") | llm | parser,
)
result = topic_analysis_chain.invoke({"topic": "远程办公"})
print("【定义】")
print(result['definition'])
print("\n【优点】")
print(result['advantages'])
print("\n【挑战】")
print(result['challenges'])
print("\n【未来趋势】")
print(result['future'])
print()
# ==================== 挑战练习 ====================
"""
挑战 1: 创建一个"面试模拟器"
- 输入岗位名称难度级别
- 输出3个面试问题 + 参考答案
- 使用结构化输出
挑战 2: 创建一个"周报生成器"
- 输入本周完成的任务列表纯文本
- 输出格式化的周报包含摘要详情下周计划
- 使用多步骤链
挑战 3: 创建一个"代码 Review Bot"
- 输入代码片段
- 并行输出安全性分析性能分析可读性分析改进建议
- 使用 RunnableParallel
"""
print("=" * 60)
print("Day 1 学习完成!")
print("=" * 60)
print("""
今日掌握的核心技能
使用 ChatOllama 调用本地模型
使用 ChatPromptTemplate 创建可复用的提示词
使用 OutputParser 将输出转为结构化数据
使用 LCEL (|) 链接多个组件
使用 RunnableParallel 并行处理
使用 RunnableLambda 插入自定义逻辑
明日预告Day 2 - LCEL 深入
- RunnableSequence 详解
- 条件分支 RunnableBranch
- 错误处理和重试
- 异步调用
""")

View File

@ -0,0 +1,82 @@
"""
Day 2 - 课程 1: LCEL 基础语法
==========================
学习目标
- 理解 RunnableSequence 的概念
- 对比 | 管道符与显式序列的写法
- 学会查看链的输入输出 schema
核心概念
- RunnableSequence: LCEL 的基础组合方式
- invoke/batch: 调用方式
- schema: 输入输出规范
"""
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnableSequence
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
print("=" * 50)
print("1. RunnableSequence vs 管道符 |")
print("=" * 50)
prompt = ChatPromptTemplate.from_template("用一句话解释{concept}")
# 管道符写法
pipe_chain = prompt | llm | parser
# 显式序列写法
seq_chain = RunnableSequence(prompt, llm, parser)
result1 = pipe_chain.invoke({"concept": "向量数据库"})
result2 = seq_chain.invoke({"concept": "向量数据库"})
print(f"管道符结果: {result1}")
print(f"序列结果: {result2}")
print()
print("=" * 50)
print("2. 查看链的 Schema")
print("=" * 50)
print(f"输入 Schema: {pipe_chain.input_schema.model_json_schema()}")
print(f"输出 Schema: {pipe_chain.output_schema.model_json_schema()}")
print()
print("=" * 50)
print("3. 链中插入 RunnableLambda")
print("=" * 50)
# 先处理输入,再生成回答
normalize = RunnableLambda(lambda x: {"concept": x["concept"].strip()})
chain = normalize | prompt | llm | parser
result = chain.invoke({"concept": " 提示词工程 "})
print(f"结果: {result}")
print()
print("=" * 50)
print("4. batch 批量调用")
print("=" * 50)
inputs = [
{"concept": "检索增强"},
{"concept": "多模态"},
{"concept": "函数调用"}
]
results = pipe_chain.batch(inputs)
for inp, res in zip(inputs, results):
print(f"{inp['concept']}: {res}")
# ==================== 练习 ====================
"""
练习 1: RunnableSequence 改成三步先加前缀再调用 LLM再解析
练习 2: 使用 RunnableLambda 给输入加上 "请用 20 字以内" 的约束
练习 3: batch 输入增加 5 个概念观察输出风格差异
"""

79
src/day2/02_parallel.py Normal file
View File

@ -0,0 +1,79 @@
"""
Day 2 - 课程 2: RunnableParallel 并行执行
=====================================
学习目标
- 理解 RunnableParallel 的输入输出结构
- 学会同时生成多个结果
- 掌握并行链的典型场景
核心概念
- RunnableParallel: 并行执行多个 Runnable
- 输出为 dict
"""
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnableParallel
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
print("=" * 50)
print("1. 基础并行:摘要 + 关键词")
print("=" * 50)
summary_prompt = ChatPromptTemplate.from_template(
"请用一句话总结以下内容:\n{text}"
)
keywords_prompt = ChatPromptTemplate.from_template(
"从文本中提取 3 个关键词,用逗号分隔:\n{text}"
)
parallel_chain = RunnableParallel(
summary=summary_prompt | llm | parser,
keywords=keywords_prompt | llm | parser,
)
text = "LLM 应用常见流程包括提示词设计、向量检索、工具调用与输出解析。"
result = parallel_chain.invoke({"text": text})
print(f"摘要: {result['summary']}")
print(f"关键词: {result['keywords']}")
print()
print("=" * 50)
print("2. 并行 + 原始输入")
print("=" * 50)
keep_input = RunnableParallel(
original=RunnableLambda(lambda x: x["text"]),
rewrite=ChatPromptTemplate.from_template("改写成更口语的表达:{text}") | llm | parser,
)
result = keep_input.invoke({"text": "LangChain 让 LLM 应用组件化并可组合。"})
print(f"原文: {result['original']}")
print(f"改写: {result['rewrite']}")
print()
print("=" * 50)
print("3. 并行处理多个视角")
print("=" * 50)
views_chain = RunnableParallel(
beginner=ChatPromptTemplate.from_template("用初学者能懂的话解释:{topic}") | llm | parser,
expert=ChatPromptTemplate.from_template("用专家视角总结要点:{topic}") | llm | parser,
analogy=ChatPromptTemplate.from_template("给出一个生活化比喻:{topic}") | llm | parser,
)
result = views_chain.invoke({"topic": "向量检索"})
print(f"初学者解释: {result['beginner']}")
print(f"专家要点: {result['expert']}")
print(f"生活化比喻: {result['analogy']}")
# ==================== 练习 ====================
"""
练习 1: 新增一个并行分支生成一个标题
练习 2: RunnableParallel 改成 dict 写法
练习 3: 在并行输出中加入原始输入的长度统计
"""

View File

@ -0,0 +1,69 @@
"""
Day 2 - 课程 3: RunnablePassthrough 数据透传
======================================
学习目标
- 理解 RunnablePassthrough 的作用
- 学会在链中保留原始输入
- 掌握 assign 用法扩展输入
核心概念
- RunnablePassthrough: 原样透传输入
- assign: 给输入添加新字段
"""
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
print("=" * 50)
print("1. 保留原始输入")
print("=" * 50)
answer_chain = ChatPromptTemplate.from_template("回答问题:{question}") | llm | parser
chain = RunnablePassthrough.assign(
answer=answer_chain
)
result = chain.invoke({"question": "什么是向量嵌入?"})
print(f"问题: {result['question']}")
print(f"回答: {result['answer']}")
print()
print("=" * 50)
print("2. 透传 + 自定义字段")
print("=" * 50)
with_meta = RunnablePassthrough.assign(
length=RunnableLambda(lambda x: len(x["text"])),
summary=ChatPromptTemplate.from_template("用一句话总结:{text}") | llm | parser,
)
result = with_meta.invoke({"text": "LCEL 可以把多个组件像管道一样连接起来。"})
print(f"文本: {result['text']}")
print(f"长度: {result['length']}")
print(f"总结: {result['summary']}")
print()
print("=" * 50)
print("3. 结合 RunnableLambda 预处理")
print("=" * 50)
normalize = RunnableLambda(lambda x: {"question": x["question"].strip()})
chain = normalize | RunnablePassthrough.assign(answer=answer_chain)
result = chain.invoke({"question": " LCEL 的优势是什么? "})
print(f"问题: {result['question']}")
print(f"回答: {result['answer']}")
# ==================== 练习 ====================
"""
练习 1: assign 添加一个字段记录问题是否包含为什么
练习 2: 输出中加入当前时间戳可以用 datetime
练习 3: 把回答链改成三步改写问题 -> 回答 -> 解析
"""

64
src/day2/04_branch.py Normal file
View File

@ -0,0 +1,64 @@
"""
Day 2 - 课程 4: RunnableBranch 条件分支
===================================
学习目标
- 理解 RunnableBranch 的分支选择逻辑
- 学会根据输入走不同链路
- 构建简单的路由器
核心概念
- RunnableBranch: 按条件选择 Runnable
- 条件函数: 输入 -> bool
"""
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableBranch, RunnableLambda
from langchain_ollama import ChatOllama
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
parser = StrOutputParser()
print("=" * 50)
print("1. 基于问题类型的分支")
print("=" * 50)
why_prompt = ChatPromptTemplate.from_template("请用逻辑清晰的方式解释原因:{question}")
how_prompt = ChatPromptTemplate.from_template("请用步骤列表回答:{question}")
what_prompt = ChatPromptTemplate.from_template("请用一句话定义:{question}")
branch = RunnableBranch(
(lambda x: "为什么" in x["question"], why_prompt | llm | parser),
(lambda x: "怎么" in x["question"] or "如何" in x["question"], how_prompt | llm | parser),
what_prompt | llm | parser,
)
questions = [
{"question": "为什么向量检索比关键字检索更好?"},
{"question": "如何搭建一个本地 LLM 服务?"},
{"question": "什么是 LCEL"},
]
for q in questions:
result = branch.invoke(q)
print(f"Q: {q['question']}")
print(f"A: {result}")
print()
print("=" * 50)
print("2. 结合预处理做路由")
print("=" * 50)
normalize = RunnableLambda(lambda x: {"question": x["question"].strip()})
chain = normalize | branch
result = chain.invoke({"question": " 怎么进行提示词优化? "})
print(f"结果: {result}")
# ==================== 练习 ====================
"""
练习 1: 新增一个分支处理对比类问题
练习 2: 把条件函数改成正则匹配
练习 3: 尝试用分支实现短问题 -> 简答 / 长问题 -> 详答
"""