langchain 基本使用

基于版本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)
相关推荐
SilentSamsara1 小时前
生成器实战:处理大文件、流水线模式与无限序列
vscode·python·青少年编程·pycharm
yaoxin5211231 小时前
402. Java 文件操作基础 - 读取二进制文件
java·开发语言·python
Hello.Reader1 小时前
ds4.c 深度解析为 DeepSeek V4 Flash 打造的本地推理引擎
c语言·开发语言
TopGames1 小时前
〖Unity GPU粒子插件〗ParticleSystem的终极性能优化方案 十倍百倍的显著提升 现有特效转GPU粒子 高性能特效方案
java·开发语言
Chase_______2 小时前
计算机数据存储全解:从底层进制转换到存储介质演进
java·开发语言·python
网络工程小王2 小时前
【LangGraph 子图(Subgraph)详解】学习笔记
java·服务器·数据库·人工智能·langchain
栉甜2 小时前
Js进阶(4)
开发语言·javascript·原型模式
小碗羊肉2 小时前
【JavaWeb | 第七篇】部门管理项目实战
java·开发语言·servlet
维诺菌3 小时前
claude code安装
java·开发语言·ai编程·calude