基于版本langchain 0.0.181
一、大模型接入
这里实现ollama 通义前问 自定义大模型接口
llm_connect.py
# -*- coding: utf-8 -*-
"""
=============================================================
01_llm_connect - LLM接入方式汇总(langchain 0.0.181)
=============================================================
对应课程:第10-13集
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# - BaseChatModel.__call__ deprecated: 建议改用 .invoke()
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
# ============================================================
# 方案A:Ollama 本地模型(内网首选)
# ============================================================
def demo_ollama_basic():
llm = ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
temperature=0.7,
)
response = llm([HumanMessage(content="你好,介绍一下自己")])
print("[Ollama]:", response.content)
# ============================================================
# 方案B:通义千问 via OpenAI兼容接口(外网)
# ============================================================
def demo_tongyi_via_openai():
llm = ChatOpenAI(
model="qwen-plus",
openai_api_key="sk-b5dd301a836744aca915366eebf81fc1",
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
temperature=0.7,
)
messages = [
SystemMessage(content="你是一个专业的Python开发工程师"),
HumanMessage(content="用30字解释什么是装饰器"),
]
response = llm(messages)
print("[通义千问]:", response.content)
# ============================================================
# 方案C:自建OpenAI兼容接口(内网部署)
# ============================================================
def demo_custom_openai_compatible():
llm = ChatOpenAI(
model="qwen2.5:32b",
openai_api_key="ollama",
openai_api_base="http://内网IP:端口/v1",
temperature=0.3,
)
response = llm([HumanMessage(content="hello")])
print("[内网模型]:", response.content)
# ============================================================
# ▶ 主函数
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("LLM接入方式演示")
print("=" * 60)
# demo_ollama_basic() #ollama
demo_tongyi_via_openai() #通义千问
# demo_custom_openai_compatible() #自定义
返回
============================================================
LLM接入方式演示
============================================================
[Ollama]: 您好!我是阿里云开发的一种超大规模语言模型,我叫通义千问。我的目标是与用户进行交流,并提供帮助和信息。我可以回答各种问题、讲述故事、编写代码、创作诗歌等。如果您有任何问题或需要帮助,请随时告诉我,我会尽力为您提供支持。
进程已结束,退出代码为 0
二、提示词模板
1、Prompt Template 提示词模板
相当于定义了多个角色的系统提示词 可以随时切换
比如你和ai聊天的时候,如果需要问某个领域的问题,比如你想问苹果多少钱,去水果店和去手机店的结果肯定不一样,但是我们又不想每次问之前都说一次,这个就是给ai一个预设的提示词
template = "你是一个{role}专家,请用{style}风格回答:{question}"
prompt = template.format(role="运维", style="简洁", question="Docker和K8s的区别")
# 只换变量,模板不变
或者我们可以预设多个角色的信息,然后根据用户提示来切换不同角色,用这个方法来读取用户需要你扮演角色的变量
# 你写一个模板,到处复用
template = PromptTemplate.from_template("用{language}写一个{function_desc}")
# 每次只换变量
print(template.format(language="Python", function_desc="读取文件的函数"))
print(template.format(language="Shell", function_desc="查看进程的函数"))
2、ChatPromptTemplate对话模板
| 类型 | 输出 | 用途 |
|---|---|---|
| PromptTemplate | 字符串 | 给普通 LLM 用 |
| ChatPromptTemplate | 消息列表 | 给 Chat 模型用(Ollama、GPT 都是这类) |
system_prompt = SystemMessagePromptTemplate.from_template(
"You are a professional {domain} engineer, answer precisely"
)
human_prompt = HumanMessagePromptTemplate.from_template(
"Question: {question}"
)
chat_template = ChatPromptTemplate.from_messages([system_prompt, human_prompt])
messages = chat_template.format_messages(domain="Python", question="How to handle high concurrency?")
返回
[SystemMessage]: You are a professional Python engineer, answer precisely
[HumanMessage]: Question: How to handle high concurrency?
案例
# 定义一个"运维问答"通用模板
ops_template = ChatPromptTemplate.from_messages([
("system", "你是一个{role},擅长{skill}"),
("human", "{question}"),
])
# 每次只换问题
messages = ops_template.format_messages(
role="K8s运维", skill="Pod排障", question="Pod一直Pending怎么办?"
)
llm(messages)
3、FewShotPromptTemplate 少样本模拟
给模型几个例子,它就能照着例子的格式回答
examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
]
example_prompt = PromptTemplate.from_template("Input: {input}, Output: {output}")
fewshot = FewShotPromptTemplate(
examples=examples,
example_prompt=example_prompt,
prefix="Give opposites:",
suffix="Input: {word}, Output:",
input_variables=["word"],
)
print(fewshot.format(word="quiet"))
返回
Give opposites:
Input: happy, Output: sad
Input: tall, Output: short
Input: quiet, Output:
模型看到前面的信息就直到你后面要输出什么
实际案例
# 让模型按指定格式输出(比如 always 输出 JSON)
examples = [
{"question": "Docker是什么?", "answer": '{"summary": "容器工具", "level": "基础"}'},
{"question": "K8s是什么?", "answer": '{"summary": "容器编排", "level": "进阶"}'},
]
# 有了这两个例子,模型就知道你期待 JSON 格式
4、完整代码
# -*- coding: utf-8 -*-
"""
02_prompt_template - Prompt Templates (langchain 0.0.181)
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.prompts import PromptTemplate
from langchain.prompts.chat import ChatPromptTemplate
from langchain.prompts.chat import SystemMessagePromptTemplate, HumanMessagePromptTemplate
from langchain.prompts.few_shot import FewShotPromptTemplate
def demo_prompt_template():
prompt = PromptTemplate.from_template("intro in {style}: {topic}")
result = prompt.format(style="concise", topic="Docker")
print("[PromptTemplate]:", result)
def demo_chat_prompt_template():
system_prompt = SystemMessagePromptTemplate.from_template(
"You are a professional {domain} engineer, answer precisely"
)
human_prompt = HumanMessagePromptTemplate.from_template(
"Question: {question}"
)
chat_template = ChatPromptTemplate.from_messages([system_prompt, human_prompt])
messages = chat_template.format_messages(domain="Python", question="How to handle high concurrency?")
print("[ChatPromptTemplate]:")
for msg in messages:
print(f" [{type(msg).__name__}]: {msg.content[:50]}")
def demo_fewshot():
examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
]
example_prompt = PromptTemplate.from_template(
"Input: {input}, Output: {output}"
)
fewshot = FewShotPromptTemplate(
examples=examples, example_prompt=example_prompt,
prefix="Give opposites:",
suffix="Input: {word}, Output:",
input_variables=["word"],
)
print("[FewShotPromptTemplate]:", fewshot.format(word="quiet"))
if __name__ == "__main__":
print("=" * 50)
print("Prompt Template Demo")
print("=" * 50)
demo_prompt_template()
demo_chat_prompt_template()
demo_fewshot()
三、格式化输出
把 LLM 返回的自由文本,转成结构化的 Python 对象(字典、列表、Pydantic 模型)
1、自定义结构
# 1. 定义你想要的数据结构
class WeatherInfo(BaseModel):
city: str
temperature: float
condition: str
# 2. 创建解析器
parser = PydanticOutputParser(pydantic_object=WeatherInfo)
# 3. 把格式说明塞进 Prompt,告诉模型怎么输出
format_instructions = parser.get_format_instructions()
# 格式说明大概是这样的:
# "输出JSON,包含字段:city (string), temperature (float), condition (string)"
# 4. 模型按要求输出 JSON 后,解析成 Python 对象
fake_json = '{"city": "Beijing", "temperature": 28.5, "condition": "Sunny"}'
result = parser.parse(fake_json)
print(result.city) # Beijing
print(result.temperature) # 28.5
案例
# 场景:让模型从用户描述中提取结构化信息
class UserInfo(BaseModel):
name: str
age: int
skill: list[str]
parser = PydanticOutputParser(pydantic_object=UserInfo)
# 把 format_instructions 加入 Prompt,模型就会输出符合格式的 JSON
2、列表输出
parser = CommaSeparatedListOutputParser()
result = parser.parse("Docker, Kubernetes, Ansible")
print(result)
# 输出:['Docker', 'Kubernetes', 'Ansible']
实际案例
# 场景:让模型列出多个关键词
# Prompt: "列出5个容器相关的技术,用逗号分隔"
# 模型输出:"Docker, Kubernetes, Podman, Containerd, CRI-O"
# Parser 直接转成列表,你的代码就能用 for 循环处理了
3、完整流水线
# 1. 定义结构
parser = PydanticOutputParser(pydantic_object=WeatherInfo)
# 2. 把格式说明塞进 Prompt
template = ChatPromptTemplate.from_messages([
("system", "你是天气助手,严格按照JSON格式输出"),
("human", "城市:{city}\n{format_instructions}"),
])
messages = template.format_messages(
city="上海",
format_instructions=parser.get_format_instructions()
)
# 3. 发给模型,拿到回复后解析
response = llm(messages)
result = parser.parse(response.content)
print(result.city, result.temperature)
4、全量代码
# -*- coding: utf-8 -*-
"""
03_output_parser - Output Parsers (langchain 0.0.181)
"""
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from pydantic import BaseModel
from typing import List
from langchain.output_parsers import PydanticOutputParser, CommaSeparatedListOutputParser
from langchain.prompts.chat import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate
class WeatherInfo(BaseModel):
city: str
temperature: float
condition: str
def demo_pydantic_parser():
parser = PydanticOutputParser(pydantic_object=WeatherInfo)
print("[Format instructions]:", parser.get_format_instructions()[:100])
template = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template("You are a weather assistant"),
HumanMessagePromptTemplate.from_template("Tell weather for {city}\n{format_instructions}"),
])
messages = template.format_messages(city="Beijing", format_instructions=parser.get_format_instructions())
print("[Prompt with format]:", messages[-1].content[:100])
fake_json = '{"city": "Beijing", "temperature": 28.5, "condition": "Sunny"}'
result = parser.parse(fake_json)
print("[Parsed result]:", result)
print("[City]:", result.city)
def demo_list_parser():
parser = CommaSeparatedListOutputParser()
result = parser.parse("Docker, Kubernetes, Ansible")
print("[List parser]:", result)
if __name__ == "__main__":
print("=" * 50)
print("Output Parser Demo")
print("=" * 50)
demo_pydantic_parser()
demo_list_parser()
返回
==================================================
Output Parser Demo
==================================================
[Format instructions]: The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an exa
[Prompt with format]: Tell weather for Beijing
The output should be formatted as a JSON instance that conforms to the JSON
[Parsed result]: city='Beijing' temperature=28.5 condition='Sunny'
[City]: Beijing
[List parser]: ['Docker', 'Kubernetes', 'Ansible']
四、链式调用
把「Prompt → LLM → OutputParser」串成一条流水线,一次调用自动走完全流程。
为什么需要它?
不用 Chain 的写法(手动一步步来):
# 每一步都要手写,麻烦
prompt = PromptTemplate.from_template("介绍:{topic}")
formatted = prompt.format(topic="Docker")
response = llm(formatted)
parsed = parser.parse(response)
用了 Chain(自动串起来):.
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(topic="Docker")
# 自动完成:填充变量 → 发LLM → 返回结果
1、LLMChain
# 1. 准备 Prompt 模板
prompt = PromptTemplate.from_template("Intro: {topic}")
# 2. 把 LLM 和 Prompt 绑在一起
chain = LLMChain(llm=llm, prompt=prompt)
# 3. 直接传变量,自动完成填充+调用
result = chain.run(topic="Kubernetes")
print(result)
核心逻辑
输入 "topic=Kubernetes"
↓
PromptTemplate 填充 → "Intro: Kubernetes"
↓
发给 LLM
↓
返回结果
2、SimpleSequentialChain 多个 Chain 串联
# Chain 1:生成标题
chain1 = LLMChain(llm=llm, prompt=PromptTemplate.from_template("Title for: {topic}"))
# Chain 2:根据标题写内容
chain2 = LLMChain(llm=llm, prompt=PromptTemplate.from_template("Write about: {input}"))
# 串起来:Chain1 的输出自动传给 Chain2
overall = SimpleSequentialChain(chains=[chain1, chain2])
result = overall.run("AI Agent")
# 步骤:生成标题 → 根据标题写内容 → 返回最终结果
实际案例
# 场景:先分类,再针对性回答
chain1 = LLMChain(llm=llm, prompt=PromptTemplate.from_template("分类:{user_input}"))
chain2 = LLMChain(llm=llm, prompt=PromptTemplate.from_template("作为{category}专家回答:{user_input}"))
overall = SimpleSequentialChain(chains=[chain1, chain2])
# 自动:先分类 → 再按分类角色回答
3、全量代码
# -*- coding: utf-8 -*-
"""
04_chains - Chains (langchain 0.0.181)
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.chains import LLMChain, SimpleSequentialChain
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
temperature=0.7,
)
def demo_llm_chain():
prompt = PromptTemplate.from_template("Intro: {topic}")
chain = LLMChain(llm=llm, prompt=prompt)
print("[LLMChain]: uncomment below to run")
# result = chain.run(topic="Kubernetes")
# print(result)
def demo_simple_sequential():
chain1 = LLMChain(llm=llm, prompt=PromptTemplate.from_template("Title for: {topic}"))
chain2 = LLMChain(llm=llm, prompt=PromptTemplate.from_template("Write about: {input}"))
overall = SimpleSequentialChain(chains=[chain1, chain2])
print("[SimpleSequentialChain]: uncomment below to run")
# result = overall.run("AI Agent")
# print(result)
if __name__ == "__main__":
print("=" * 50)
print("Chains Demo")
print("=" * 50)
demo_llm_chain()
demo_simple_sequential()
五、流式响应
让模型的输出像打字机一样逐字显示,而不是等全部生成完才一次性返回
为什么要流式输出?
不用流式(等待时间长,体验差):
用户:介绍一下Docker
(等待3秒...)
模型:Docker是一个开源容器化平台,由Solomon Hykes创立...(一次性返回)
用流式(边生成边显示,体验好):
用户:介绍一下Docker
模型:Docker是一个开源容器化平台,由Solomon Hykes创立...
(每个字实时显示,不用等)
1、基础流式输出
llm = ChatOpenAI(..., streaming=True)
print("AI: ", end="", flush=True)
for chunk in llm.stream("用20个字介绍 Docker"):
text = chunk.content if hasattr(chunk, 'content') else str(chunk)
print(text, end="", flush=True)
print() # 换行
streaming=True开启流式
llm.stream()返回生成器,每次 yield 一小块
flush=True强制立即显示,不用等缓冲区满
2、ChatModel 流式(最常用)
chat = get_llm()
messages = [HumanMessage(content="用一句话解释什么是 AI Agent")]
for chunk in chat.stream(messages):
if chunk.content:
print(chunk.content, end="", flush=True)
3、Chain 流式
chain = LLMChain(llm=llm, prompt=prompt)
for output in chain.stream({"topic": "Kubernetes"}):
text_chunk = output.get("text", "")
print(text_chunk, end="", flush=True)
4、异步流式(async/await)
什么时候用异步? 需要同时处理多个用户请求的时候(比如 Web 服务)。
async def demo_async_streaming():
async for chunk in llm.astream("用20个字介绍什么是 DevOps"):
content = chunk.content if hasattr(chunk, 'content') else str(chunk)
print(content, end="", flush=True)
5、全量代码
# -*- coding: utf-8 -*-
"""
=============================================================
05_streaming --- 流式响应(langchain 0.0.181)
=============================================================
对应课程:第17集
涵盖:
- LLM 流式输出(stream)
- ChatModel 流式输出(stream)
- 异步流式(async/await)
- 实战:FastAPI SSE 流式接口伪代码
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage
import asyncio
def get_llm():
return ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
temperature=0.7,
streaming=True,
)
# ============================================================
# 方案A:LLM.stream() 流式输出
# ============================================================
def demo_llm_streaming():
llm = get_llm()
print("[LLM 流式输出 - 逐字显示]:")
print("AI: ", end="", flush=True)
for chunk in llm.stream("用20个字介绍 Docker"):
text = chunk.content if hasattr(chunk, 'content') else str(chunk)
print(text, end="", flush=True)
print()
def demo_llm_streaming_collect():
llm = get_llm()
full_text = ""
print("[流式输出并收集完整文本]:")
for chunk in llm.stream("列出3个 K8s 常用命令"):
content = chunk.content if hasattr(chunk, 'content') else str(chunk)
full_text += content
print(content, end="", flush=True)
print("\n\n[收集到的完整文本]:", full_text)
return full_text
# ============================================================
# 方案B:ChatModel.stream() 流式输出
# ============================================================
def demo_chat_model_streaming():
chat = get_llm()
print("[ChatModel 流式输出]:")
messages = [HumanMessage(content="用一句话解释什么是 AI Agent")]
for chunk in chat.stream(messages):
if chunk.content:
print(chunk.content, end="", flush=True)
print()
# ============================================================
# 方案C:LLMChain 链式流式
# ============================================================
def demo_chain_streaming():
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
llm = get_llm()
prompt = PromptTemplate.from_template("用一句话介绍:{topic}")
chain = LLMChain(llm=llm, prompt=prompt)
print("[LLMChain 流式输出]:")
print("AI: ", end="", flush=True)
for output in chain.stream({"topic": "Kubernetes"}):
text_chunk = output.get("text", "")
print(text_chunk, end="", flush=True)
print()
# ============================================================
# 方案D:异步流式(async/await)
# ============================================================
async def demo_async_streaming():
llm = get_llm()
print("[异步流式输出]:")
print("AI: ", end="", flush=True)
async for chunk in llm.astream("用20个字介绍什么是 DevOps"):
content = chunk.content if hasattr(chunk, 'content') else str(chunk)
print(content, end="", flush=True)
print()
# ============================================================
# 主函数
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("流式响应演示(langchain 0.0.181)")
print("=" * 60)
print()
print("⚠️ 以下演示需要 Ollama 在运行(http://localhost:11434)")
print()
print("取消下面注释即可运行对应演示:")
print()
demo_llm_streaming()
# demo_llm_streaming_collect()
# demo_chat_model_streaming()
# demo_chain_streaming()
print("异步流式需要:asyncio.run(demo_async_streaming())")
六、memory --- 记忆系统
让模型记住之前说过的话,实现多轮对话(像 ChatGPT 那样有上下文)。
1、 全量记忆
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
# 往记忆里写对话
memory.chat_memory.add_user_message("我是一名运维工程师")
memory.chat_memory.add_ai_message("好的,我会用运维风格回答")
# 查看当前记住了什么
for msg in memory.chat_memory.messages:
print(msg.content)
返回
[HumanMessage]: 我是一名运维工程师
[AIMessage]: 好的,我会用运维风格回答
缺点 对话越长,消耗的 Token 越多,最终会超限。不适合长对话。
2、滑动窗口⭐ 最常用
memory = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=3, # 只记住最近 3 轮对话
)
# 写入 5 轮对话
for i in range(5):
memory.chat_memory.add_user_message(f"问题 {i}")
memory.chat_memory.add_ai_message(f"回答 {i}")
# 实际上只保留了最近 3 轮
print(len(memory.chat_memory.messages)) # 输出:6(3轮 × 2条)
为什么最常用?
控制了 Token 消耗
又保留了最近的上下文
线上系统的首选方案
3、在 Chain 中使用 Memory
prompt = PromptTemplate.from_template(
"历史:{chat_history}\n\n用户:{input}\n\nAI:"
)
memory = ConversationBufferMemory(memory_key="chat_history")
chain = LLMChain(llm=llm, prompt=prompt, memory=memory)
# 第一次问
chain.run(input="我叫小明")
# 第二次问,Chain 自动把历史塞进 {chat_history}
chain.run(input="我叫什么?") # 模型能回答"小明"
关键点: Chain 会自动管理记忆的读写,你不用手动
add_user_message。
4、方法对比
| 类型 | 记忆范围 | Token 消耗 | 适用场景 |
|---|---|---|---|
| BufferMemory | 全部 | 高 | 短对话 |
| WindowMemory | 最近 N 轮 | 中 | 线上最常用 |
| TokenBufferMemory | 限制 Token 数 | 可控 | Token 受限场景 |
| SummaryBufferMemory | 摘要+最近 | 低 | 长对话总结 |
5、全量代码
# -*- coding: utf-8 -*-
"""
=============================================================
06_memory --- 记忆系统(langchain 0.0.181)
=============================================================
对应课程:第18-21集
涵盖:
- ConversationBufferMemory(全量记忆)
- ConversationBufferWindowMemory(滑动窗口)
- ConversationTokenBufferMemory(Token预算)
- ConversationSummaryBufferMemory(摘要+最近)
- 在Chain中使用Memory
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.memory import (
ConversationBufferMemory,
ConversationBufferWindowMemory,
ConversationTokenBufferMemory,
ConversationSummaryBufferMemory,
)
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
def get_llm():
return ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
temperature=0.7,
)
# ============================================================
# 方案A:ConversationBufferMemory(全量记忆)
# ============================================================
def demo_buffer_memory():
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True,
)
memory.chat_memory.add_user_message("我是一名运维工程师")
memory.chat_memory.add_ai_message("好的,我会用运维风格回答")
print("[BufferMemory 当前记忆]:")
for msg in memory.chat_memory.messages:
print(f" [{type(msg).__name__}]: {msg.content[:40]}")
print()
print("[说明] BufferMemory 会无限增长,长对话会超Token限制")
def demo_window_memory():
memory = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=3,
)
for i in range(5):
memory.chat_memory.add_user_message(f"问题 {i}")
memory.chat_memory.add_ai_message(f"回答 {i}")
print("[WindowMemory k=3]:")
print(f" 消息数: {len(memory.chat_memory.messages)}")
print("[说明] WindowMemory 是最常用的线上方案")
def demo_memory_in_chain():
llm = get_llm()
prompt = PromptTemplate.from_template(
"历史:{chat_history}\n\n用户:{input}\n\nAI:"
)
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=False,
)
chain = LLMChain(llm=llm, prompt=prompt, memory=memory)
print("[Chain+Memory]: 取消下面注释即可运行")
# chain.run(input="我叫小明")
# chain.run(input="我叫什么?")
if __name__ == "__main__":
print("=" * 60)
print("记忆系统演示(langchain 0.0.181)")
print("=" * 60)
demo_buffer_memory()
demo_window_memory()
demo_memory_in_chain()
七、embeddings --- 文本嵌入 + RAG
把文字转成向量(数字列表),然后通过相似度搜索找到最相关的内容 ,这是 RAG(检索增强生成)的核心技术。
为什么需要它?
普通 LLM 不知道你的私有知识(公司文档、个人笔记等)。
0、RAG 方案
用户提问
↓
把问题转成向量
↓
去知识库里找最相关的文档(向量相似度搜索)
↓
把找到的文档塞进 Prompt
↓
LLM 基于这些文档回答
这样 LLM 就能回答它没训练过的内容。
1、Embedding 基础(文字 → 向量)
emb = OllamaEmbeddings()
# 单条文本 → 向量(一个数字列表)
v = emb.embed_query("Kubernetes 管理容器")
print(len(v)) # 比如 768(向量维度)
print(v[:5]) # 前5个数字,比如 [0.12, -0.34, 0.56, ...]
# 多条文本 → 多个向量
vs = emb.embed_documents(["Docker", "K8s", "Python"])
print(len(vs)) # 3 个向量
什么是向量?
"Kubernetes" → [0.12, -0.34, 0.56, 0.78, ...] (768维)
"K8s" → [0.11, -0.33, 0.55, 0.77, ...] (768维)
"苹果" → [0.01, 0.82, -0.12, 0.33, ...] (768维)
含义相近的词,向量数值也相近(相似度高)
2、FAISS 向量库 + 相似度搜索
emb = OllamaEmbeddings()
texts = [
"Kubernetes 用于容器编排",
"Docker 用于容器化应用",
"Python 是一门编程语言",
"LangChain 是 AI Agent 开发框架",
]
# 把文字向量化,存入 FAISS(本地向量数据库)
store = FAISS.from_texts(texts, emb)
# 搜索:找和"容器管理"最相近的 2 条
results = store.similarity_search("容器管理", k=2)
for doc in results:
print(doc.page_content)
返回
Kubernetes 用于容器编排
Docker 用于容器化应用
实际用处
**# 把公司所有文档向量化存起来
用户问问题时,先去向量库搜相关文档,再让 LLM 基于这些文档回答
这就是 RAG!**
3、完整 RAG 流水线 ⭐ 最核心
# 1. 准备知识库文档
docs = [
Document(page_content="公司规定:下班必须关空调,违者罚款200元。"),
Document(page_content="年假:入职满1年可休5天,满10年可休10天。"),
Document(page_content="报销流程:先OA审批,再提交发票。"),
]
# 2. 文本分割(文档太长,切成小块)
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_docs = splitter.split_documents(docs)
# 3. 向量化,存入 FAISS
store = FAISS.from_documents(split_docs, emb)
# 4. 用户提问,检索相关文档
query = "下班要关空调吗?"
related = store.similarity_search(query, k=2)
context = "\n".join(d.page_content for d in related)
# 5. 把相关文档塞进 Prompt,让 LLM 基于这些内容回答
prompt = f"参考以下资料回答问题:\n{context}\n\n问题:{query}"
answer = llm.predict(prompt)
print(answer) # "根据公司规定,下班必须关空调,违者罚款200元。"
4、RAG 完整流程图
知识库文档
↓ 文本分割
小块文档
↓ embedding 向量化
存入 FAISS 向量库
↓
用户提问 "下班要关空调吗?"
↓ embedding
问题向量
↓ 相似度搜索
找到相关文档
↓
拼接:相关文档 + 用户问题 → Prompt
↓
LLM 回答(基于提供的文档)
5、OllamaEmbeddings 自定义类说明
langchain 0.0.181 没有 OllamaEmbeddings,所以代码里自己写了一个
class OllamaEmbeddings:
def __init__(self, model="nomic-embed-text"):
self.model = model
self.base_url = "http://localhost:11434"
def embed_query(self, text):
# 调用 Ollama 的 /api/embed 接口
resp = requests.post(f"{self.base_url}/api/embed",
json={"model": self.model, "input": text})
return resp.json()["embeddings"][0]
需要先拉取嵌入模型:
ollama pull nomic-embed-text
6、内网离线方案:BM25 关键词检索(不需要嵌入模型)
问题背景
nomic-embed-text 模型在内网无法拉取,或者内网完全隔离,无法安装任何外部模型。
解决方案:BM25
BM25 是一种经典的关键词检索算法,不需要任何嵌入模型、不需要向量、纯 Python 实现。
对比:
| FAISS 向量检索 | BM25 关键词检索 | |
|---|---|---|
| 依赖 | 需要嵌入模型(nomic-embed-text 等) | 只需要 pip install rank-bm25 |
| 原理 | 语义相似度("电脑"能找到"计算机") | 关键词命中 + 统计学权重 |
| 优势 | 语义理解强 | 完全离线,内网可用 |
| 劣势 | 需要嵌入模型 | 无法识别同义词 |
安装(只需一次):
pip install rank-bm25
代码示例:
from rank_bm25 import BM25Okapi
import re
# 简单中文分词(不依赖 jieba,纯 Python)
def tokenize(text):
text = re.sub(r'[^\w\s\u4e00-\u9fff]', ' ', text)
chars = list(text)
tokens = []
for word in text.split():
if word.isascii():
tokens.append(word.lower())
i = 0
while i < len(chars):
c = chars[i]
if '\u4e00' <= c <= '\u9fff':
tokens.append(c)
if i + 1 < len(chars) and '\u4e00' <= chars[i + 1] <= '\u9fff':
tokens.append(c + chars[i + 1])
i += 1
return tokens
# 知识库
docs = ["Pod 常见故障:ImagePullBackOff", "重启服务流程..."]
tokenized = [tokenize(d) for d in docs]
bm25 = BM25Okapi(tokenized)
# 搜索
query = "Pod 启动失败"
scores = bm25.get_scores(tokenize(query))
top_idx = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[0]
print(docs[top_idx]) # 找到 "Pod 常见故障:ImagePullBackOff"
7、全量代码
# -*- coding: utf-8 -*-
"""
=============================================================
07_embeddings --- 文本嵌入 + RAG(langchain 0.0.181)
=============================================================
对应课程:第22-24集
涵盖:
- Embeddings 嵌入模型调用
- FAISS 向量存储
- 文本分割 + 向量化
- 完整 RAG 问答流水线
- BM25 离线检索方案(内网专用,无需嵌入模型)
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# ============================================================
import warnings
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from langchain.embeddings.base import Embeddings
import requests
import re
# ============================================================
# 自定义 OllamaEmbeddings(走 Ollama /api/embed 接口)
# ============================================================
class OllamaEmbeddings(Embeddings):
def __init__(self, model="nomic-embed-text", base_url="http://localhost:11434"):
self.model = model
self.base_url = base_url.rstrip('/')
def embed_query(self, text):
resp = requests.post(
f"{self.base_url}/api/embed",
json={"model": self.model, "input": text},
)
resp.raise_for_status()
return resp.json()["embeddings"][0]
def embed_documents(self, texts):
resp = requests.post(
f"{self.base_url}/api/embed",
json={"model": self.model, "input": texts},
)
resp.raise_for_status()
return resp.json()["embeddings"]
# ============================================================
# BM25 离线检索方案(内网专用,不需要嵌入模型)
# ============================================================
# 安装:pip install rank-bm25
# 原理:纯关键词匹配 + BM25 统计学权重排序,不依赖任何嵌入模型
try:
from rank_bm25 import BM25Okapi
_BM25_AVAILABLE = True
except ImportError:
_BM25_AVAILABLE = False
class BM25KnowledgeBase:
"""
离线知识库搜索类(使用 BM25 算法)
完全不依赖嵌入模型、网络请求、Ollama,内网隔离环境也能用
"""
def __init__(self, documents: list):
self.documents = documents
self.texts = [doc.page_content for doc in documents]
if not _BM25_AVAILABLE:
raise RuntimeError("rank-bm25 未安装,请执行:pip install rank-bm25")
tokenized_texts = [self._tokenize(t) for t in self.texts]
self.bm25 = BM25Okapi(tokenized_texts)
def _tokenize(self, text: str) -> list:
"""简单中文分词:单字 + 双字词,不依赖 jieba"""
text = re.sub(r'[^\w\s\u4e00-\u9fff]', ' ', text)
chars = list(text)
tokens = []
for word in text.split():
if word.isascii():
tokens.append(word.lower())
i = 0
while i < len(chars):
c = chars[i]
if '\u4e00' <= c <= '\u9fff':
tokens.append(c)
if i + 1 < len(chars) and '\u4e00' <= chars[i + 1] <= '\u9fff':
tokens.append(c + chars[i + 1])
i += 1
return tokens
def similarity_search(self, query: str, k: int = 2) -> list:
"""搜索最相关的 k 条文档"""
query_tokens = self._tokenize(query)
scores = self.bm25.get_scores(query_tokens)
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:k]
return [self.documents[i] for i in top_indices if scores[i] > 0]
# ============================================================
# 方案A:embedding 基础演示(需要 Ollama + nomic-embed-text)
# ============================================================
def demo_embed_basic():
print("[方案A] embed_query / embed_documents")
emb = OllamaEmbeddings()
v = emb.embed_query("Kubernetes 管理容器")
print(f" [embed_query] 维度: {len(v)}, 前5维: {v[:5]}")
vs = emb.embed_documents(["Docker", "K8s", "Python"])
print(f" [embed_documents] 数量: {len(vs)}")
# ============================================================
# 方案B:FAISS 向量存储 + 相似度搜索(需要 Ollama + nomic-embed-text)
# ============================================================
def demo_faiss_search():
print("[方案B] FAISS 向量检索")
emb = OllamaEmbeddings()
texts = [
"Kubernetes 用于容器编排",
"Docker 用于容器化应用",
"Python 是一门编程语言",
"LangChain 是 AI Agent 开发框架",
]
store = FAISS.from_texts(texts, emb)
results = store.similarity_search("容器管理", k=2)
print(" [FAISS 搜索 '容器管理']:")
for doc in results:
print(f" - {doc.page_content}")
# ============================================================
# 方案C:完整 RAG 流水线(需要 Ollama + nomic-embed-text)
# ============================================================
def demo_rag_pipeline():
print("[方案C] 完整 RAG 流水线")
from langchain.chat_models import ChatOpenAI
emb = OllamaEmbeddings()
docs = [
Document(page_content="公司规定:下班必须关空调,违者罚款200元。"),
Document(page_content="年假:入职满1年可休5天,满10年可休10天。"),
Document(page_content="报销流程:先OA审批,再提交发票。"),
]
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_docs = splitter.split_documents(docs)
store = FAISS.from_documents(split_docs, emb)
llm = ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
)
query = "下班要关空调吗?"
related = store.similarity_search(query, k=2)
context = "\n".join(d.page_content for d in related)
prompt = f"参考以下资料回答问题:\n{context}\n\n问题:{query}"
answer = llm.predict(prompt)
print(f" 问题:{query}")
print(f" 答案:{answer}")
# ============================================================
# 方案D:BM25 离线检索(内网专用,不需要嵌入模型)
# ============================================================
def demo_bm25_search():
"""
完全离线的 RAG 方案,不依赖 Ollama、不需要嵌入模型
适合内网、隔离环境、或无法拉取 nomic-embed-text 的场景
"""
print("[方案D] BM25 离线检索")
if not _BM25_AVAILABLE:
print(" [跳过] rank-bm25 未安装,请先执行:pip install rank-bm25")
return
docs = [
Document(page_content="Pod 常见故障:ImagePullBackOff(镜像拉取失败)、CrashLoopBackOff(启动后崩溃)、Pending(无法调度)"),
Document(page_content="排查 Pod Pending:1. kubectl describe pod 查看事件;2. 检查资源是否充足;3. 检查节点是否有污点"),
Document(page_content="重启服务流程:1. 确认影响范围;2. 通知相关人员;3. 执行 kubectl rollout restart deployment/<名称>;4. 验证服务恢复"),
Document(page_content="日志查询命令:kubectl logs <pod名> -n <命名空间>,查看最近日志加 --tail=100"),
Document(page_content="公司告警规则:CPU 使用率超过 80% 持续 5 分钟触发 P1 告警,内存超过 90% 触发 P2 告警"),
]
kb = BM25KnowledgeBase(docs)
query = "Pod 启动失败怎么排查"
results = kb.similarity_search(query, k=2)
print(f" [BM25 搜索 '{query}']:")
for i, doc in enumerate(results, 1):
print(f" [{i}] {doc.page_content}")
# 结合 LLM 回答(可选,需要 Ollama)
print(" [BM25 + LLM 问答]:")
try:
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
)
context = "\n".join(d.page_content for d in results)
prompt = f"参考以下资料回答问题,只参考资料里的内容:\n{context}\n\n问题:{query}"
answer = llm.predict(prompt)
print(f" 问题:{query}")
print(f" 答案:{answer}")
except Exception as e:
print(f" [跳过] LLM 不可用:{e}")
if __name__ == "__main__":
print("=" * 60)
print("文本嵌入 + RAG 演示(langchain 0.0.181)")
print("=" * 60)
print()
print("方案A:embed_query / embed_documents(需要 Ollama + nomic-embed-text)")
demo_embed_basic()
print()
print("方案B:FAISS 向量检索(需要 Ollama + nomic-embed-text)")
demo_faiss_search()
print()
print("方案C:完整 RAG 流水线(需要 Ollama + nomic-embed-text)")
print(" → 取消下面注释即可运行")
# demo_rag_pipeline()
print()
print("方案D:BM25 离线检索(不需要 Ollama、不需要嵌入模型,内网专用)")
demo_bm25_search()
八、tools_and_agent --- 工具调用 + Agent
给 LLM 挂载外部工具 (计算器、搜索、查数据库等),让模型能调用工具完成任务,而不只是聊天
为什么需要它?
普通 LLM 只能"说话":
用户:123 × 456 等于多少?
模型:让我算一下,大概是 56088。
有了 Tool,LLM 能"动手":
用户:123 × 456 等于多少?
模型:我需要调用 Calculator 工具
→ 调用 Calculator(123*456)
→ 返回 56088
模型:结果是 56088。
核心概念
| 概念 | 说明 |
|---|---|
| Tool | 一个外部函数(比如计算器、查天气、搜数据库) |
| Agent | 能自主选工具、自动调用多个工具的 LLM |
1、定义工具
# 工具1:计算文本长度
def get_text_length(text: str) -> str:
"""Calculate text length"""
return f"Text length: {len(text)} chars"
# 工具2:计算器
def calculator(expr: str) -> str:
result = eval(expr)
return f"Result: {result}"
# 包装成 LangChain Tool
tools = [
Tool(name="TextLength", func=get_text_length, description="Calculate text length"),
Tool(name="Calculator", func=calculator, description="Calculate math expression"),
]
description很重要!Agent 靠这个描述决定"要不要用这个工具"。
2. 创建 Agent
agent = initialize_agent(
tools=tools,
llm=llm,
agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True, # 打印 Agent 的思考过程
)
# Agent 自动决定要不要用工具、用哪个工具
agent.run("Calculate 123 * 456")
Agent 的思考流程(verbose=True 会打印这些)
> Entering new AgentExecutor chain...
问:123 * 456 等于多少?
思考:我需要用计算器。
动作:Calculator
输入:123 * 456
观察:Result: 56088
思考:我知道答案了。
最终答案:123 × 456 = 56088。
> Finished chain.
AgentType 说明
| 类型 | 说明 |
|---|---|
ZERO_SHOT_REACT_DESCRIPTION |
根据工具描述自主选择,最常用 |
CONVERSATIONAL_REACT_DESCRIPTION |
带记忆的多轮对话 Agent |
实际用处
# 场景:运维 Agent,能查服务器状态
tools = [
Tool(name="CheckCPU", func=check_cpu, description="查看CPU使用率"),
Tool(name="CheckDisk", func=check_disk, description="查看磁盘空间"),
Tool(name="RestartService", func=restart_service, description="重启指定服务"),
]
agent = initialize_agent(tools=tools, llm=llm, agent_type=...)
agent.run("我的服务挂了,帮我排查")
# Agent 会自动:查CPU → 查磁盘 → 重启服务
3、完整代码
# -*- coding: utf-8 -*-
"""
08_tools_and_agent - Tools + Agent (langchain 0.0.181)
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from langchain.agents import initialize_agent, AgentType
from langchain.tools import Tool
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
temperature=0.7,
)
def get_text_length(text: str) -> str:
"""Calculate text length"""
return f"Text length: {len(text)} chars"
def calculator(expr: str) -> str:
"""Calculate math expression like 2+3*5"""
try:
result = eval(expr, {"__builtins__": {}}, {})
return f"Result: {result}"
except Exception as e:
return f"Error: {e}"
tools = [
Tool(name="TextLength", func=get_text_length, description="Calculate text length"),
Tool(name="Calculator", func=calculator, description="Calculate math expression"),
]
def demo_agent():
agent = initialize_agent(
tools=tools, llm=llm,
agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)
print("[Agent]: uncomment below to run")
# agent.run("Calculate 123 * 456")
if __name__ == "__main__":
print("=" * 50)
print("Tools + Agent Demo")
print("=" * 50)
demo_agent()
九、langgraph --- 手动状态机(替代 LangGraph)
当 Agent 的逻辑变复杂(有多个步骤、需要条件判断),用状态机来精确控制流程。
langchain 0.0.181 没有 LangGraph,所以用「手动状态机」来替代。
为什么需要它?
**简单 Agent(08 的方式)**适合单一问题,逻辑简单。
用户问 → Agent 选工具 → 返回答案
复杂 Agent(需要状态机):
用户问
↓
节点1:判断意图(天气 or 聊天 or 其他)
↓
节点2:根据意图路由到不同处理逻辑
↓
节点3:生成回答
↓
结束
当流程有多个步骤、需要条件分支时,就需要状态机。
LangGraph 是什么?
LangGraph 是 langchain 官方出的复杂 Agent 流程编排工具,用「图」来表示步骤和分支。
但因为 langchain 0.0.181 太老,没有 LangGraph,所以本文件用「手动状态机」实现同样的功能。
1. 定义状态(State)(手动状态机)
class State(TypedDict):
user_input: str # 用户输入
intent: str # 识别出的意图
answer: str # 最终答案
next_node: str # 下一步去哪个节点
状态就是一个共享的字典,所有节点都能读写它。
2. 定义节点(每个节点是一个函数)
# 节点1:判断意图
def node_classify(state):
prompt = f"Classify intent (weather/chat/other): {state['user_input']}"
intent = llm.predict(prompt).strip()
return {"intent": intent, "next_node": "respond"}
# 节点2:生成回答
def node_respond(state):
prompt = f"User said: {state['user_input']}"
answer = llm.predict(prompt)
return {"answer": answer, "next_node": "END"}
3. 运行状态机(主循环)
nodes = {
"classify": node_classify, # 节点名 → 函数
"respond": node_respond,
}
state = {
"user_input": "How is the weather today?",
"intent": "",
"answer": "",
"next_node": "classify" # 从 classify 节点开始
}
while state["next_node"] != "END":
print(f" -> Node: {state['next_node']}")
node_func = nodes[state["next_node"]]
state = node_func(state) # 执行节点,更新 state
print(f" Answer: {state['answer']}")
执行流程
start → classify 节点 → 判断意图 → next_node = "respond"
↓
respond 节点 → 生成回答 → next_node = "END"
↓
结束,输出 answer
和 LangGraph 的对应关系
| 手动状态机 | LangGraph |
|---|---|
State 类 |
StateGraph |
nodes 字典 |
graph.add_node() |
next_node 字段 |
graph.add_edge() |
| while 循环 | graph.compile().invoke() |
实际用处
# 场景:运维 Agent,根据告警级别走不同流程
# 节点1:解析告警
# 节点2:if 级别=严重 → 节点3(立刻通知)
# if 级别=普通 → 节点4(记录日志)
# 节点5:生成处理报告
# 用状态机可以精确控制这个流程
# 这是 Agent 开发的核心技能
# -*- coding: utf-8 -*-
"""
09_langgraph - Manual State Machine (langchain 0.0.181)
"""
# ============================================================
# 忽略 langchain 弃用警告(langchain 0.0.181 兼容)
# 说明:以下警告不影响功能,仅为未来版本迁移提醒
# - LangChainDeprecationWarning: API 在新版本中已迁移
# 本项目目标版本为 0.0.181,忽略这些警告以保持输出干净
# ============================================================
import warnings
# 直接覆盖 showwarning 阻止警告输出(warnings.filterwarnings 无法拦截 LangChainDeprecationWarning)
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
from typing import TypedDict
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(
model="qwen2.5:1.5b",
openai_api_key="ollama",
openai_api_base="http://localhost:11434/v1",
temperature=0.7,
)
class State(TypedDict):
user_input: str
intent: str
answer: str
next_node: str
def node_classify(state):
prompt = f"Classify intent (weather/chat/other): {state["user_input"]}\nOne word only"
intent = llm.predict(prompt).strip()
return {"intent": intent, "next_node": "respond"}
def node_respond(state):
prompt = f"User said: {state["user_input"]}\nReply in one sentence"
answer = llm.predict(prompt)
return {"answer": answer, "next_node": "END"}
def demo_state_machine():
nodes = {"classify": node_classify, "respond": node_respond}
state = {"user_input": "How is the weather today?", "intent": "", "answer": "", "next_node": "classify"}
while state["next_node"] != "END":
print(f" -> Node: {state['next_node']}")
state = nodes[state["next_node"]](state)
print(f" Answer: {state["answer"]}")
if __name__ == "__main__":
print("=" * 50)
print("State Machine Demo (替代LangGraph)")
print("=" * 50)
print("[Uncomment below to run]")
# demo_state_machine()
全局说明
01_llm_connect → 接入模型(会调模型了)
02_prompt_template → 管理 Prompt(会写模板了)
03_output_parser → 解析输出(拿到结构化数据了)
04_chains → 串成流水线(自动化了)
05_streaming → 流式输出(体验优化了)
06_memory → 记住上下文(多轮对话了)
07_embeddings → 向量搜索 + RAG(有知识库了)
08_tools_and_agent → 挂载工具(能"动手"了)
09_langgraph → 复杂流程控制(能处理复杂任务了)
10、完整案例
# -*- coding: utf-8 -*-
"""
====================================================================
完整实战案例:智能运维助手(Intelligent Ops Assistant)
====================================================================
本文件整合了 langchain 0.0.181 的核心功能,构建一个能:
1. 记住对话上下文(Memory)
2. 从知识库搜索答案(RAG / BM25 关键词检索)
3. 调用运维工具(Tools / Agent)
4. 流式输出回复(Streaming)
5. 输出结构化结果(Output Parser)
只需确保 Ollama 在运行,模型名改成你自己的,即可直接运行。
====================================================================
"""
# =====================================================================
# 第一步:屏蔽 langchain 弃用警告(不影响功能,只是警告烦人)
# =====================================================================
import warnings
def _silent_showwarning(*args, **kwargs):
pass
warnings.showwarning = _silent_showwarning
# =====================================================================
# 第二步:导入所有需要的库
# =====================================================================
from langchain.chat_models import ChatOpenAI # 接入 Ollama(走 OpenAI 兼容接口)
from langchain.schema import HumanMessage, AIMessage # 对话消息类型
from langchain.prompts.chat import ( # Prompt 模板(对话型)
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
from langchain.memory import ConversationBufferWindowMemory # 记忆系统(滑动窗口,最常用)
from langchain.schema import Document # 文档类型
from langchain.output_parsers import PydanticOutputParser # 输出解析器(转 JSON/对象)
from langchain.tools import Tool # 工具封装(让 LLM 能"动手")
from pydantic import BaseModel, Field # Pydantic:定义输出数据结构
import re
# =====================================================================
# 第三步:纯离线知识检索 --- BM25 关键词搜索
# =====================================================================
# 说明:BM25 是一种经典的信息检索算法,不需要任何嵌入模型,
# 纯靠"关键词匹配 + 统计学权重"来排序文档。
# 适合内网、离线、完全隔离的环境。
#
# 安装方式(只需要做一次):
# pip install rank-bm25
#
# 原理:
# 用户问题:"Pod 启动失败怎么办"
# → 分词得到:["Pod", "启动", "失败", "怎么办"]
# → 在知识库中找包含这些词最多的文档
# → 按 BM25 权重排序返回
#
# 和 FAISS 向量检索的对比:
# 向量检索:语义相近就匹配(如"计算机"能找到"电脑")
# BM25 :关键词匹配,精确度高,但无法识别同义词
# 生产环境通常两种结合使用,本项目只用 BM25 保证离线可用。
# =====================================================================
try:
from rank_bm25 import BM25Okapi
_BM25_AVAILABLE = True
except ImportError:
_BM25_AVAILABLE = False
print("[警告] rank-bm25 未安装,RAG 搜索将无法工作")
print("[提示] 请执行:pip install rank-bm25")
class BM25KnowledgeBase:
"""
离线知识库搜索类(使用 BM25 算法)
完全不依赖任何嵌入模型、网络请求、内网无法拉取镜像的场景也能用
"""
def __init__(self, documents: list):
"""
:param documents: Document 对象列表(每个 Document.page_content 是文本内容)
"""
self.documents = documents
self.texts = [doc.page_content for doc in documents]
if not _BM25_AVAILABLE:
raise RuntimeError("rank-bm25 未安装,请执行:pip install rank-bm25")
# 分词:用简单的正则按空格/标点分割(中文按单字+双字词混合)
# jieba 更准确但需要额外安装,这里用纯 Python 方案避免依赖
tokenized_texts = [self._tokenize(t) for t in self.texts]
self.bm25 = BM25Okapi(tokenized_texts)
def _tokenize(self, text: str) -> list:
"""
简单分词:中文按单字和双字词切分,英文按空格切分
这是最简方案,不依赖 jieba 等中文分词库
"""
# 清理标点符号
text = re.sub(r'[^\w\s\u4e00-\u9fff]', ' ', text)
chars = list(text)
tokens = []
# 英文单词保留
for word in text.split():
if word.isascii():
tokens.append(word.lower())
# 中文:单字 + 叠字词(两个相同汉字,如"运维运维")
i = 0
while i < len(chars):
c = chars[i]
if '\u4e00' <= c <= '\u9fff':
tokens.append(c)
# 尝试加一个相邻汉字组成双字词
if i + 1 < len(chars) and '\u4e00' <= chars[i + 1] <= '\u9fff':
tokens.append(c + chars[i + 1])
i += 1
return tokens
def similarity_search(self, query: str, k: int = 2) -> list:
"""
搜索最相关的 k 条文档
:param query: 用户的问题
:param k: 返回几条(默认 2 条)
:return: Document 对象列表,按相关度从高到低排序
"""
query_tokens = self._tokenize(query)
# BM25 打分
scores = self.bm25.get_scores(query_tokens)
# 取分数最高的 k 个索引
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:k]
return [self.documents[i] for i in top_indices if scores[i] > 0]
# =====================================================================
# 第四步:定义输出数据结构(用 Pydantic)
# =====================================================================
class OpsResponse(BaseModel):
"""运维助手的标准回复格式"""
status: str = Field(description="处理状态:success(成功)/ need_tool(需要调用工具)/ ask_user(需要追问用户)")
message: str = Field(description="给用户的回复内容")
suggestion: str = Field(description="操作建议,比如『建议执行 kubectl get pods』")
# =====================================================================
# 第五步:初始化 LLM(接入 Ollama)
# =====================================================================
llm = ChatOpenAI(
model="qwen2.5:1.5b", # Ollama 中的模型名,改成你自己的
openai_api_key="ollama", # 本地 Ollama 不校验 key,随便填
openai_api_base="http://localhost:11434/v1", # Ollama 的 OpenAI 兼容地址
temperature=0.3, # 创造性:0=很严谨,1=很发散,运维场景用 0.3 比较稳
streaming=True, # 开启流式输出(逐字显示)
)
# =====================================================================
# 第六步:准备知识库(RAG 的核心数据)
# =====================================================================
# 说明:BM25 知识库,不需要嵌入模型,不需要 nomic-embed-text,
# 纯关键词匹配,内网完全隔离环境也能用。
knowledge_docs = [
Document(page_content="公司 K8s 集群信息:生产集群 3 台 Master,8 台 Node,版本 1.24"),
Document(page_content="Pod 常见故障:ImagePullBackOff(镜像拉取失败)、CrashLoopBackOff(启动后崩溃)、Pending(无法调度)"),
Document(page_content="排查 Pod Pending:1. kubectl describe pod 查看事件;2. 检查资源是否充足;3. 检查节点是否有污点"),
Document(page_content="公司告警规则:CPU 使用率超过 80% 持续 5 分钟触发 P1 告警,内存超过 90% 触发 P2 告警"),
Document(page_content="重启服务流程:1. 确认影响范围;2. 通知相关人员;3. 执行 kubectl rollout restart deployment/<名称>;4. 验证服务恢复"),
Document(page_content="日志查询命令:kubectl logs <pod名> -n <命名空间>,查看最近日志加 --tail=100"),
Document(page_content="公司值班电话:P1 告警打 13800001111,P2 告警打 13800002222,非工作时间自动转接"),
]
# 初始化 BM25 知识库(无需联网、无需嵌入模型)
knowledge_base = BM25KnowledgeBase(knowledge_docs)
# =====================================================================
# 第七步:定义运维工具(Agent 可以调用的功能)
# =====================================================================
def check_pod_status(pod_name: str) -> str:
"""检查 K8s Pod 状态(模拟函数)"""
return f"[模拟] Pod {pod_name} 状态:Running,已运行 3 天,CPU 使用率 45%"
def search_log(keywords: str) -> str:
"""搜索日志关键词(模拟函数)"""
return f"[模拟] 找到 {keywords} 相关日志 12 条,最新一条:ERROR: connection timeout to 10.0.0.5:3306"
def get_cluster_info(dummy: str) -> str:
"""获取集群信息(模拟函数)"""
return "[模拟] 集群状态:健康。Running Pods: 47,CPU 总使用率 62%,内存使用率 71%"
tools = [
Tool(name="CheckPodStatus", func=check_pod_status,
description="检查指定 Pod 的运行状态,输入 Pod 名称,比如 'my-app-pod-xxx'"),
Tool(name="SearchLog", func=search_log,
description="在日志中搜索关键词,输入关键词字符串,比如 'timeout' 或 'ERROR'"),
Tool(name="GetClusterInfo", func=get_cluster_info,
description="获取 K8s 集群整体状态信息,无需参数,输入空字符串即可"),
]
# =====================================================================
# 第八步:初始化记忆系统
# =====================================================================
memory = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=5,
)
# =====================================================================
# 第九步:构建主 Prompt 模板
# =====================================================================
system_template = SystemMessagePromptTemplate.from_template("""
你是一个专业的运维助手,擅长 K8s、Docker、Linux 运维。
回答要求:
- 准确、简洁、可操作
- 如果知识库里有相关信息,优先依据知识库回答
- 如果需要调用工具才能回答,输出 status=need_tool
- 如果问题不清楚,输出 status=ask_user 并追问
- 输出必须是合法 JSON,包含字段:status, message, suggestion
""")
human_template = HumanMessagePromptTemplate.from_template("""
## 对话历史:
{chat_history}
## 相关知识库内容:
{context}
## 用户问题:
{user_input}
请严格按照 JSON 格式输出,不要加 ```json ``` 标记。
""")
main_prompt = ChatPromptTemplate.from_messages([system_template, human_template])
# =====================================================================
# 第十步:初始化输出解析器
# =====================================================================
output_parser = PydanticOutputParser(pydantic_object=OpsResponse)
system_template_with_format = SystemMessagePromptTemplate.from_template("""
你是一个专业的运维助手,擅长 K8s、Docker、Linux 运维。
回答要求:
- 准确、简洁、可操作
- 优先依据知识库回答
- 输出必须是合法 JSON
## 输出格式要求(必须严格遵守):
{format_instructions}
""")
main_prompt_with_parser = ChatPromptTemplate.from_messages([
system_template_with_format,
human_template,
])
# =====================================================================
# 第十一步:定义状态机
# =====================================================================
def make_state(user_input: str) -> dict:
return {
"user_input": user_input,
"chat_history": "",
"context": "",
"raw_response": "",
"parsed_result": None,
"next_node": "rag_search",
}
def node_rag_search(state: dict) -> dict:
"""
节点1:用 BM25 从知识库搜索相关文档
"""
# 用关键词匹配找最相关的 2 条文档
docs = knowledge_base.similarity_search(state["user_input"], k=2)
context = "\n".join([d.page_content for d in docs])
print(f"[节点 RAG 搜索] 找到 {len(docs)} 条相关文档")
return {**state, "context": context, "next_node": "call_llm"}
def node_call_llm(state: dict) -> dict:
"""
节点2:调用 LLM 生成回复
"""
history_str = ""
for msg in memory.chat_memory.messages:
if isinstance(msg, HumanMessage):
role = "用户"
elif isinstance(msg, AIMessage):
role = "助手"
else:
role = "系统"
history_str += f"{role}:{msg.content}\n"
messages = main_prompt_with_parser.format_messages(
format_instructions=output_parser.get_format_instructions(),
chat_history=history_str or "(无历史对话)",
context=state.get("context", "(无相关文档)"),
user_input=state["user_input"],
)
print("[节点 LLM 调用] 正在生成回复...")
full_text = ""
for chunk in llm.stream(messages):
if hasattr(chunk, 'content') and chunk.content:
print(chunk.content, end="", flush=True)
full_text += chunk.content
print()
try:
clean_text = full_text.strip()
if clean_text.startswith("```"):
clean_text = clean_text.split("\n", 1)[-1]
if clean_text.endswith("```"):
clean_text = clean_text.rsplit("```", 1)[0]
clean_text = clean_text.strip()
parsed = output_parser.parse(clean_text)
print(f"[解析成功] status={parsed.status}, message={parsed.message[:30]}...")
return {**state, "raw_response": full_text, "parsed_result": parsed,
"next_node": "use_tool" if parsed.status == "need_tool" else "respond"}
except Exception as e:
print(f"[解析失败] {e},直接进入回复")
return {**state, "raw_response": full_text, "parsed_result": None, "next_node": "respond"}
def node_use_tool(state: dict) -> dict:
"""节点3:调用运维工具"""
print("[节点 工具调用] 正在调用工具...")
tool_result = get_cluster_info("")
print(f"[工具返回] {tool_result}")
follow_up_prompt = f"工具返回结果:{tool_result}\n请用一句话告诉用户结果。"
follow_up_response = llm.predict(follow_up_prompt)
return {**state, "raw_response": follow_up_response, "next_node": "respond"}
def node_respond(state: dict) -> dict:
"""节点4:展示回复给用户,并存入记忆"""
result = state.get("parsed_result")
raw = state.get("raw_response", "")
if result:
final_message = result.message
print(f"\n[最终回复] {final_message}")
print(f"[操作建议] {result.suggestion}")
else:
final_message = raw
print(f"\n[最终回复] {final_message}")
memory.chat_memory.add_user_message(state["user_input"])
memory.chat_memory.add_ai_message(final_message)
return {**state, "next_node": "END"}
NODE_REGISTRY = {
"rag_search": node_rag_search,
"call_llm": node_call_llm,
"use_tool": node_use_tool,
"respond": node_respond,
}
# =====================================================================
# 第十二步:主对话循环
# =====================================================================
def run_conversation(user_input: str):
print("\n" + "=" * 60)
print(f"用户:{user_input}")
print("=" * 60)
state = make_state(user_input)
while state["next_node"] != "END":
node_name = state["next_node"]
print(f"\n→ 进入节点:{node_name}")
node_func = NODE_REGISTRY[node_name]
updates = node_func(state)
state = {**state, **updates}
print("\n" + "=" * 60)
print("本轮对话结束")
print("=" * 60)
# =====================================================================
# 第十三步:命令行交互入口
# =====================================================================
if __name__ == "__main__":
print("=" * 60)
print(" 智能运维助手 - 完整实战案例")
print(" 基于 langchain 0.0.181 + Ollama + BM25(离线知识检索)")
print("=" * 60)
print("\n输入 'exit' 或 'quit' 退出")
print("输入 'history' 查看对话历史")
print("输入 'clear' 清空对话历史")
print()
while True:
try:
user_input = input("你:").strip()
except (EOFError, KeyboardInterrupt):
print("\n再见!")
break
if not user_input:
continue
if user_input.lower() in ("exit", "quit", "退出", "q"):
print("再见!")
break
if user_input.lower() == "history":
print("\n--- 对话历史 ---")
for msg in memory.chat_memory.messages:
if isinstance(msg, HumanMessage):
role = "用户"
elif isinstance(msg, AIMessage):
role = "助手"
else:
role = "系统"
print(f" [{role}] {msg.content[:80]}")
print()
continue
if user_input.lower() == "clear":
memory.chat_memory.clear()
print("对话历史已清空\n")
continue
run_conversation(user_input)