diff --git a/LANGCHAIN_LEARN.md b/LANGCHAIN_LEARN.md new file mode 100644 index 0000000..f35e792 --- /dev/null +++ b/LANGCHAIN_LEARN.md @@ -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 | + +--- + +## 核心概念速查 + +### LCEL(LangChain 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 文件 diff --git a/README.md b/README.md new file mode 100644 index 0000000..abf191a --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +从零训练一个迷你语言模型 + +第 1 天:字符级 RNN(GRU) + +环境要求 +- Python 3.10+ +- PyTorch(Apple Silicon 支持 MPS) + +安装与准备 +- python -m venv .venv +- source .venv/bin/activate +- pip install torch + +运行 +- python day1_char_rnn.py + +你应该看到 +- 每 100 步输出一次 loss +- 每 200 步输出一次采样文本 + +说明 +- 这是一个极小的数据集和模型,只为验证完整训练流程。 +- 下一步我们会把硬编码文本替换为文件,并加入评估指标。 diff --git a/day1_char_rnn.py b/day1_char_rnn.py new file mode 100644 index 0000000..0e53185 --- /dev/null +++ b/day1_char_rnn.py @@ -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 和目标 y(y 是 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() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..610fc9c --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/src/day1/01_hello_llm.py b/src/day1/01_hello_llm.py new file mode 100644 index 0000000..11afa72 --- /dev/null +++ b/src/day1/01_hello_llm.py @@ -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 轮的对话历史,测试模型是否能记住上下文 +""" diff --git a/src/day1/02_prompt_template.py b/src/day1/02_prompt_template.py new file mode 100644 index 0000000..76642c9 --- /dev/null +++ b/src/day1/02_prompt_template.py @@ -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) + +# 方式1:from_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: 创建一个"故事生成器"模板,支持指定主角、背景和风格 +""" diff --git a/src/day1/03_output_parser.py b/src/day1/03_output_parser.py new file mode 100644 index 0000000..7dd915b --- /dev/null +++ b/src/day1/03_output_parser.py @@ -0,0 +1,198 @@ +""" +Day 1 - 课程 3: Output Parser(输出解析器) +========================================= + +学习目标: +- 理解为什么需要输出解析 +- 掌握常用解析器:StrOutputParser、JsonOutputParser +- 学会使用 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: 修改代码审查解析器,添加"是否建议合并"字段 +""" + diff --git a/src/day1/04_chain_basics.py b/src/day1/04_chain_basics.py new file mode 100644 index 0000000..5ca53f0 --- /dev/null +++ b/src/day1/04_chain_basics.py @@ -0,0 +1,193 @@ +""" +Day 1 - 课程 4: Chain 基础(链式调用) +==================================== + +学习目标: +- 理解 LCEL(LangChain 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: 创建一个链,输入关键词,输出包含标题、正文、标签的文章结构 +""" + diff --git a/src/day1/04_exec.py b/src/day1/04_exec.py new file mode 100644 index 0000000..3c2b924 --- /dev/null +++ b/src/day1/04_exec.py @@ -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}") \ No newline at end of file diff --git a/src/day1/05_practice.py b/src/day1/05_practice.py new file mode 100644 index 0000000..3106bdc --- /dev/null +++ b/src/day1/05_practice.py @@ -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 +- 错误处理和重试 +- 异步调用 +""") diff --git a/src/day2/01_lcel_basics.py b/src/day2/01_lcel_basics.py new file mode 100644 index 0000000..e152d38 --- /dev/null +++ b/src/day2/01_lcel_basics.py @@ -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 个概念,观察输出风格差异 +""" diff --git a/src/day2/02_parallel.py b/src/day2/02_parallel.py new file mode 100644 index 0000000..ecc4db4 --- /dev/null +++ b/src/day2/02_parallel.py @@ -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: 在并行输出中加入原始输入的长度统计 +""" diff --git a/src/day2/03_passthrough.py b/src/day2/03_passthrough.py new file mode 100644 index 0000000..2b7b329 --- /dev/null +++ b/src/day2/03_passthrough.py @@ -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: 把回答链改成三步:改写问题 -> 回答 -> 解析 +""" diff --git a/src/day2/04_branch.py b/src/day2/04_branch.py new file mode 100644 index 0000000..0d8abc6 --- /dev/null +++ b/src/day2/04_branch.py @@ -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: 尝试用分支实现“短问题 -> 简答 / 长问题 -> 详答” +"""