Skip to content

博客

SSE and Streamable HTTP

MCP(Model Context Protocol) 里两种流式传输机制的差异是什么?以及为什么社区从 SSE(Server-Sent Events) 慢慢转向 Streamable HTTP


1. SSE 和 Streamable HTTP 在 MCP 里的区别

Section titled “1. SSE 和 Streamable HTTP 在 MCP 里的区别”
  • 协议:基于 HTTP/1.x 的单向推送,服务器持续通过 Content-Type: text/event-stream 推送事件。
  • 特性
    • 只能 服务器 → 客户端 单向流。
    • 事件是基于文本的、UTF-8 编码。
    • 连接保持长时间打开,数据通过 \n\n 分隔。
    • 浏览器原生支持 EventSource,实现简单。
  • 在 MCP 中的使用:主要用于模型响应的流式传输(像 ChatGPT 一样逐字输出)。

  • 协议:利用 HTTP 响应体本身作为流通道,客户端读取分块(chunked transfer)或 HTTP/2 帧。

  • 特性

    • 不依赖额外事件封装,直接在响应体中发送 JSON/二进制片段。
    • 支持 双向交互(尤其在 HTTP/2/HTTP/3 中可以用 request/response stream 组合实现)。
    • 传输效率更高,数据结构更灵活(不限制必须是 UTF-8 文本)。
  • 在 MCP 中的使用:作为替代 SSE 的新流式响应标准,既能流式传输模型输出,也能嵌入更多元数据。


2. 为什么 MCP 社区在放弃 SSE,转向 Streamable HTTP

Section titled “2. 为什么 MCP 社区在放弃 SSE,转向 Streamable HTTP”
问题SSE 的限制Streamable HTTP 的优势
双向流只能服务器推送,不能客户端回流数据可以在 HTTP/2 多路复用下实现双向流
传输类型只能是 UTF-8 文本,不能直接传二进制支持二进制、JSON、混合数据
分帧结构需要手动封装成 event: 格式,解析麻烦可以直接发送结构化 JSON/Protobuf
HTTP 版本限制基本只依赖 HTTP/1.1,HTTP/2 支持有限原生支持 HTTP/1.1、HTTP/2、HTTP/3
中间代理兼容性某些反向代理、CDN 会断开长连接分块响应在代理和缓存系统中更健壮
浏览器生态浏览器支持好,但 MCP 不局限于浏览器客户端在 CLI、SDK、嵌入式中更易实现

核心原因: MCP 协议不仅要在浏览器跑,还要在 CLI、桌面端、服务端之间通信,SSE 的单向文本流限制和代理兼容性问题在多端环境下变得明显,而 Streamable HTTP 更通用、更灵活、更高效。


MCP 社区转向 Streamable HTTP 是因为它能在 HTTP/1.1/2/3 上提供更通用、更高性能、更灵活的流式通信能力,而不受 SSE 单向文本流的限制。

4. SSE vs Streamable HTTP 在 MCP 协议中的数据流对比图

Section titled “4. SSE vs Streamable HTTP 在 MCP 协议中的数据流对比图”
┌───────────────────────────────────┐
│ 客户端 (MCP Client) │
└───────────────┬───────────────────┘
┌───────────────┴───────────────────┐
│ │
SSE(单向推送) Streamable HTTP(分块流)
───────────────────────────────────────────────────────────────────────
HTTP 请求:
GET /stream POST /inference (或 GET)
Content-Type: application/json
服务器响应:
Content-Type: text/event-stream
event: data Content-Type: application/json
data: { "token": "你" } ┌───────────── 分块1 ──────────────┐
│ { "token": "你" } │
event: data ├───────────── 分块2 ──────────────┤
data: { "token": "好" } │ { "token": "好" } │
├───────────── 分块3 ──────────────┤
... │ { "token": "啊" } │
└──────────────────────────────────┘
方向:
仅服务器 → 客户端 双向可能:客户端也可流式上传数据(HTTP/2/3)
数据类型:
UTF-8 文本 JSON / 二进制 / 混合

RAG:知识库分段

在构建知识库时,分段(Chunking)是一个至关重要的步骤,尤其是在与大型语言模型(LLM)结合使用,例如在检索增强生成(RAG)系统中。分段的目的是将长文本分割成较小的、可管理的块,以便:

  1. 适应LLM的上下文窗口限制: LLM有输入Token数量的限制,过长的文本需要被切分。
  2. 提高检索相关性: 较小的、语义连贯的块更容易在向量搜索中被精确匹配到。
  3. 减少计算成本: 处理更小的块可以降低嵌入(embedding)和检索的计算量。
  4. 降低“幻觉”风险: 提供更精确、上下文相关的知识块有助于LLM生成更准确的回答。

以下是常见的知识库分段方式,以及它们各自的优缺点和适用场景:

1. 基于固定大小的分段 (Fixed-Size Chunking)

Section titled “1. 基于固定大小的分段 (Fixed-Size Chunking)”

这是最简单、最直接的分段方式。

  • 方式: 将文档按照固定的字符数或Token数进行切分,通常会设置一个重叠量(overlap)。
    • 示例: 每500个字符切成一个块,重叠100个字符。
  • 优点:
    • 实现简单,处理速度快。
    • 适用于任何文本类型。
  • 缺点:
    • 破坏语义完整性: 可能会在一个句子的中间或段落的中间进行切分,导致上下文缺失或语义中断。
    • 检索到的块可能不包含完整的概念,降低RAG的效果。
  • 适用场景:
    • 对语义完整性要求不高的简单文本。
    • 初步处理或需要快速处理大量文档时。

2. 基于标点符号或结构的分段 (Delimiter-Based / Structural Chunking)

Section titled “2. 基于标点符号或结构的分段 (Delimiter-Based / Structural Chunking)”

这种方式尝试在文本的自然断点处进行切分,以保留更多的语义完整性。

  • 方式:
    • 句子级分段: 将文档按句子切分(例如,遇到句号、问号、叹号等)。
    • 段落级分段: 将文档按段落切分(例如,遇到连续的换行符)。
    • 标题/章节级分段: 根据文档的标题、副标题、章节等结构信息进行切分。这通常需要解析文档的格式(如Markdown、HTML、PDF结构等)。
  • 优点:
    • 语义完整性较好: 尽量保持句子或段落的完整性,减少语义中断。
    • 实现相对简单,处理效率较高。
  • 缺点:
    • 无法处理长句子/长段落: 如果一个句子或段落太长,仍然可能超出LLM的上下文限制。
    • 依赖文档结构: 对于结构不明确或混杂的文档,效果可能不佳。
  • 适用场景:
    • 大多数散文体文档(文章、报告、博客)。
    • 有清晰标题和章节结构的文档。

3. 递归字符文本拆分器 (Recursive Character Text Splitter)

Section titled “3. 递归字符文本拆分器 (Recursive Character Text Splitter)”

这是 LangChain 等框架中推荐的通用文本分段策略,结合了固定大小和结构化切分的思想。

  • 方式: 尝试按一系列分隔符(如 "\n\n""\n"" """)递归地切分文本。它会优先使用更高级别(通常表示更大语义单元)的分隔符进行切分,如果切分后的块仍然过大,则继续使用下一个分隔符进行切分,直到所有块都小于指定大小,并通常带有重叠。
  • 优点:
    • 兼顾语义和长度: 尽可能保持语义完整性的同时,确保块大小符合要求。
    • 通用性强,适用于多种文本类型。
    • 提供重叠机制,有助于保留上下文信息。
  • 缺点:
    • 对于高度非结构化或语义关联性复杂的文本,可能仍有局限。
  • 适用场景:
    • 目前最常用且推荐的通用分段策略,适用于绝大多数知识库场景。

4. 父子分段 (Parent-Child Chunking / Small-to-Large Chunking)

Section titled “4. 父子分段 (Parent-Child Chunking / Small-to-Large Chunking)”

这是一种更高级的策略,旨在平衡检索精度和上下文完整性。

  • 方式:
    • 小块检索: 创建较小的、语义独立的子块用于向量搜索(例如,单个句子或短段落)。
    • 大块提供上下文: 为每个小块关联一个更大的“父块”(可能是原始文档的整个段落、章节,甚至整个文档),当小块被检索到时,将对应的父块提供给LLM作为上下文。
  • 优点:
    • 高检索精度: 小块有助于精确匹配用户查询。
    • 丰富上下文: 大块确保LLM获得足够的背景信息,生成更准确、连贯的回答。
    • 有效解决“金句问题”:当查询只需要文档中的某一句时,直接提供该句及其上下文,而不是整个大文档。
  • 缺点:
    • 实现复杂度较高。
    • 存储和处理成本增加(需要存储两套块:子块的嵌入和父块的文本)。
  • 适用场景:
    • 需要高精度检索和丰富上下文的复杂文档(如研究论文、法律文件、技术手册)。
    • 对RAG系统性能要求较高的场景。

这是一种利用大模型或嵌入模型的能力来智能切分的策略。

  • 方式:
    • 将文本切分成句子或小段落。
    • 对每个小单元生成嵌入向量。
    • 计算相邻单元之间嵌入向量的相似度(例如,余弦相似度)。
    • 在相似度低于某个阈值的地方(表示语义发生了较大转变)进行切分。
    • 有时会结合LLM来判断最佳切分点。
  • 优点:
    • 高度保留语义完整性: 确保每个块都包含一个相对完整的、连贯的概念或主题。
    • 更智能的切分: 不依赖于固定的规则,而是根据文本的实际含义进行调整。
  • 缺点:
    • 实现复杂,计算成本较高(需要生成大量嵌入并进行相似度计算)。
    • 依赖于嵌入模型和可能的LLM的性能。
  • 适用场景:
    • 对语义连贯性和RAG系统效果要求极高的场景。
    • 处理复杂、多主题或非结构化文本时效果更佳。

没有“最好”的方式,只有“最适合”的方式。选择哪种分段策略取决于以下几个因素:

  • 您的数据类型和结构: 文档是结构化的(如手册、报告)还是非结构化的(如聊天记录、邮件)?
  • LLM的上下文窗口大小: 您的LLM能处理多长的文本?
  • 性能要求: 您对检索的精度和生成答案的质量有什么要求?
  • 计算资源和成本: 您愿意投入多少计算资源进行分段和存储?
  • 实现复杂度: 您有多少时间和精力投入到分段策略的实现上?

一般推荐:

  • 通用场景(推荐首选): 递归字符文本拆分器 (Recursive Character Text Splitter) 是一个非常好的起点。它在简单性和效果之间取得了很好的平衡,并且易于在 LangChain 等框架中实现。
  • 追求更高精度和上下文完整性: 考虑 父子分段。它能够有效解决“金句问题”,并为LLM提供更丰富的上下文。
  • 处理复杂语义或对效果有极致要求: 探索 语义分段。虽然计算成本高,但它在保留语义连贯性方面表现最佳。
  • 对于极度简单的文本或初步测试: 固定大小或基于标点符号的分段可以作为快速解决方案。

在实际操作中,通常会尝试不同的分段策略和参数(如块大小、重叠量),并通过**评估RAG系统的性能(例如,检索召回率、答案的准确性和连贯性)**来找到最适合您知识库的最佳分段方案。

Python: Annotated和Pydantic

typing.Annotated 在 Python3.9 版中引入,用于为类型注解添加元数据。作为 PEP 593 -- Flexible function and variable annotations 的一部分被添加到 typing 模块中。使用 AnnotatedPython 环境必须是 3.9 或更高版本。 typing.Annotated 的核心作用是为现有的 Python 类型添加元数据(metadata)。它本身并不执行任何运行时的类型检查或验证。可以将 Annotated 看作是一个类型装饰器,它允许你将额外的信息(可以是任何 Python 对象) 与一个类型关联起来。

这个设计的主要目的是为了让第三方工具能够更好地理解类型注解,从而提供更丰富的功能,例如类型检查、文档生成等,且无需修改 Python 的核心类型系统。通过使用 Annotated,开发者可以更灵活地表达类型信息,使代码更加清晰和可维护,

Pydantic 是一个 Python 库,主要用于数据验证和解析。它通过 Python 的类型提示 (type hints) 来定义数据的结构,并在运行时验证数据的有效性。

以下是 Pydantic 的主要用途和优点:

  • 数据验证 (Data Validation): 这是 Pydantic 最核心的功能。你可以使用 Pydantic 模型清晰地定义预期的数据结构和类型。当接收到外部数据(例如,来自 API 请求、配置文件、用户输入等)时,Pydantic 会自动根据你定义的模型进行验证。如果数据不符合预期类型或约束,Pydantic 会抛出详细的错误信息,告诉你哪些字段出了问题以及具体原因。
  • 数据解析 (Data Parsing): 除了验证,Pydantic 还能将输入数据解析成 Python 对象。即使输入数据是字符串、数字或其他格式,Pydantic 也会尝试将其转换为模型中定义的 Python 类型。例如,一个字符串 “123” 可以被解析成 int 类型。
  • 数据序列化 (Data Serialization): Pydantic 模型可以将 Python 对象序列化为其他格式,如 JSON。这对于构建 API 接口非常有用,因为你需要将 Python 对象转换为 JSON 格式发送给客户端。
  • 类型提示的强大利用: Pydantic 充分利用了 Python 3.6+ 引入的类型提示。通过类型提示,你可以清晰地表达数据的预期结构,这不仅方便了 Pydantic 的验证和解析,也提高了代码的可读性和可维护性,并能与 MyPy 等静态类型检查工具良好集成。
  • JSON Schema 生成: Pydantic 可以根据你的模型自动生成 JSON Schema。JSON Schema 是一种描述 JSON 数据结构的标准化方式,这对于 API 文档生成和与其他系统进行数据交互非常有用。
  • 与 Web 框架集成: Pydantic 与许多流行的 Python Web 框架(如 FastAPI、Starlette)无缝集成。FastAPI 甚至在底层 heavily rely on Pydantic 来处理请求和响应的数据验证和序列化。
  • 自定义验证器 (Custom Validators): 除了基本的类型验证,Pydantic 还允许你定义自定义的验证逻辑,以满足更复杂的业务规则。你可以编写函数来检查字段的值是否符合特定的要求。
  • 数据清洗和转换 (Data Cleaning and Transformation): 在验证过程中,Pydantic 还可以对数据进行清洗和转换。例如,你可以定义将字符串自动去除首尾空格,或者将日期字符串转换为 datetime 对象。

总而言之,Pydantic 主要用于确保你的 Python 应用程序接收和处理的数据是有效和符合预期的。它可以帮助你减少因数据格式错误而引发的 bug,提高代码的健壮性和可靠性,并简化数据处理相关的开发工作。

一些常见的应用场景包括:

  • API 开发: 验证和解析 API 请求的输入数据,以及序列化 API 的输出数据。
  • 数据处理管道: 确保从不同来源读取的数据符合预期的格式和类型。
  • 配置文件解析: 加载和验证应用程序的配置文件。
  • 用户输入验证: 验证 Web 表单或命令行工具接收到的用户输入。
  • 机器学习模型的数据预处理: 确保输入模型的数据具有正确的结构和类型。
  • typing.Annotated: 是 Python 类型提示系统的一部分,提供了一种为现有类型添加元数据的标准方法。它本身不执行任何运行时操作,而是让第三方工具能够读取和利用这些额外的元数据。
  • Pydantic: 专注于数据建模、验证、解析和序列化,通过定义 Pydantic 模型并利用类型提示来实现这些功能。它是一个功能强大的库,用于处理外部数据并确保其符合预期的结构和类型。

在实际应用中,Annotated 经常和 Pydantic 组合使用,以增强 Pydantic 模型的功能。虽然 Pydantic 也可以单独使用,但结合 Annotated 可以提供更灵活和标准化的方式来定义模型的行为。

基本的数据验证和解析: 当你只需要定义数据的基本结构和类型,并进行简单的验证和解析时,可以直接使用 Pydantic 模型。例如:

from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
signup_ts: float | None = None

在这种情况下,Pydantic 会根据类型提示自动进行类型检查和基本的数据转换。

函数参数和返回值注释 (使用 Field):

from pydantic import Field
def search_database(
query: str = Field(description="Search query string"),
limit: int = Field(10, description="Maximum number of results", ge=1, le=100)
) -> list:
...

与这种方式相比,如果只需要需要定义简单的数据结构,并且不需要复杂的验证或序列化,可以直接使用 Python 的类型提示:

def search_database(
query: str,
limit: int,
) -> list:
...
  • 优点:
    • 结构化数据: Field 来自 pydantic 或类似的库,它允许你定义更丰富的数据类型信息,例如描述、默认值、验证规则(ge, le)。
    • 自动化文档和验证: 这些信息可以被用于自动生成 API 文档(例如使用 FastAPI),以及在运行时进行数据验证。
    • IDE 支持: 现代 IDE 可以利用这些信息提供更好的代码提示和错误检查。
  • 缺点:
    • 依赖外部库: 你需要引入 pydantic 或类似的库。
    • 更冗长: 代码量相对较多,对于简单的类型注释可能显得过于繁琐。
from typing import Annotated
from pydantic import Field
def analyze_metrics(
# number with range constraints
count: Annotated[int, Field(description="数据数量", ge=1, le=100)],
ratio: Annotated[float, Field(description="比率", ge=0, le=1)],
# String with pattern and length constraints
user_id: Annotated[
str, Field(description="用户ID 格式: XX0000", pattern=r"^[a-z]{2}\d{4}$")
],
comment: Annotated[str, Field(description="备注", min_len=3, max_length=500)] = "",
factor: Annotated[float, Field(description="因子", multiple_of=5)] = 10,
) -> dict[str, float]:
"""分析一组数据的统计信息."""
pass
  • 优点:
    • 类型提示和元数据: 使用 Annotated 可以将类型提示与元数据(例如 Field)结合起来。
    • 灵活性: 允许你添加多个元数据,不仅限于描述和验证规则。
    • 标准化: Annotated 是 Python 3.9 引入的标准库 typing 的一部分,因此不需要额外的依赖(除非 Field 来自外部库)。
  • 缺点:
    • 可读性略差: 相对于简单的类型提示,Annotated 的语法可能稍微复杂一些。
    • 需要 Python 3.9+: Annotated 是 Python 3.9 引入的特性。

Annotated 通常与 Pydantic 结合使用,以增强 Pydantic 模型字段的定义,添加自定义验证逻辑、元数据等。

  • 基本数据建模: 如果你只需要定义简单的数据结构,并且不需要复杂的验证或序列化,可以直接使用 Python 的类型提示。
  • 数据验证和解析: 如果你需要对输入数据进行严格的验证和解析,并将其转换为 Python 对象,选择 Pydantic。
  • 自定义验证和元数据: 如果你需要在 Pydantic 模型中添加自定义的验证逻辑或额外的元数据(例如字段描述、别名、格式等),那么使用 Annotated 结合 Pydantic 是推荐的做法。
  • 第三方库集成: 如果你使用的第三方库支持 Annotated,并且你想利用类型提示中的元数据来配置这些库的行为,那么可以使用 Annotated。例如,Pydantic 利用 Annotated 来支持自定义验证器 (PlainValidator) 和字段配置 (Field).

使用 `huggingface-cli` 下载 GGUF 格式模型给 Ollama 运行

前提条件:

  1. 安装 huggingface-cli:
    Terminal window
    pip install huggingface_hub
  2. 了解模型在 Hugging Face Hub 上的位置: 你需要知道模型仓库名称和 GGUF 文件名。GGUF 文件通常以 .gguf 结尾。

步骤:

  1. 登录 Hugging Face (可选但推荐):

    Terminal window
    huggingface-cli login

    按照提示输入你的 token (在 Hugging Face 网站的 “Settings” -> “Access Tokens” 中创建或找到)。

  2. 使用 huggingface-cli download 命令下载 GGUF 文件:

    Terminal window
    huggingface-cli download <repository_id> <filename> --local-dir <destination_directory> --local-dir-use-symlinks False

    参数解释:

    • <repository_id>: Hugging Face Hub 上模型的仓库 ID (例如 TheBloke/Llama-3-8B-Instruct-GGUF).
    • <filename>: 你想要下载的 GGUF 文件的确切文件名 (例如 llama-3-8b-instruct.Q4_K_M.gguf).
    • --local-dir <destination_directory>: 你希望将 GGUF 文件保存到的本地目录 (例如 ~/models/llama3).
    • --local-dir-use-symlinks False: 设置为 False 以完整复制文件。

    示例:

    Terminal window
    huggingface-cli download TheBloke/Llama-3-8B-Instruct-GGUF llama-3-8b-instruct.Q4_K_M.gguf --local-dir ~/models/llama3 --local-dir-use-symlinks False

    查找 GGUF 文件名:

    • 访问模型在 Hugging Face Hub 上的页面。
    • 浏览 “Files and versions” 标签。
    • 找到以 .gguf 结尾的文件并复制其确切名称。
  3. 为 Ollama 创建 Modelfile (如果需要):

    在与 GGUF 文件相同的目录下创建一个名为 Modelfile 的文本文件,并添加内容:

    FROM ./<你的GGUF文件名>.gguf

    <你的GGUF文件名>.gguf 替换为实际下载的 GGUF 文件名。

    示例 Modelfile (假设 GGUF 文件是 llama-3-8b-instruct.Q4_K_M.gguf~/models/llama3 目录下):

    FROM ./llama-3-8b-instruct.Q4_K_M.gguf
  4. 使用 Ollama 运行模型:

    导航到包含 GGUF 文件和 Modelfile 的目录 (例如 cd ~/models/llama3),然后创建 Ollama 模型:

    Terminal window
    ollama create <你的模型名称> -f ./Modelfile

    <你的模型名称> 替换为你希望在 Ollama 中使用的模型名称 (例如 llama3-instruct-q4).

    运行模型:

    Terminal window
    ollama run <你的模型名称>

    例如:

    Terminal window
    ollama run llama3-instruct-q4

    根据 Ollama 的文档和常见实践,模型名称通常需要满足以下条件:

    • 只能包含小写字母、数字和连字符 (-)。
    • 不能包含大写字母。
    • 不能包含下划线 (_) 或其他特殊字符。
    • 不能为空。

总结:

使用 huggingface-cli download 下载 GGUF 文件,创建 Modelfile 指向该文件,然后使用 ollama createollama run 在 Ollama 中运行模型。请确保文件名和路径正确。

使用 LlamaIndex 和 Milvus 检索增强生成 (RAG)

这里将要介绍使用本地部署的LLM,如何使用LlamaIndex构建RAG系统。

rag

主要流程:

workflow

  • RAG 是一种基于检索增强生成(Retrieval-Augmented Generation)的技术,它通过检索相关文档来增强生成模型的输出。RAG可以用于各种任务,包括问答、摘要、翻译等。

  • LlamaIndex 是一个简单、灵活的数据框架,用于将自定义数据源连接到大型语言模型(LLMs)。它支持多种数据源,包括文本文件、数据库、API等,并且可以轻松地扩展到新的数据源。它还提供了丰富的API,使得构建RAG系统变得非常简单。

  • Ollama 是一个开源的LLM部署工具,允许用户在本地电脑上运行各种大型语言模型(LLMs)。它的主要特点包括:

    1. 本地化运行 - 让用户可以在自己的电脑上离线运行AI模型,而不需要连接到云服务
    2. 支持多种模型 - 包括Llama 2、Mistral、Vicuna等多种开源语言模型
    3. 简单易用 - 提供了简单的命令行界面和API接口
    4. 资源效率 - 针对桌面环境进行了优化,减少了资源消耗
    5. 隐私保护 - 因为模型在本地运行,所以数据不会发送到外部服务器
  • Milvus 是一个开源的向量数据库,可以用于存储和检索高维向量数据。它支持多种向量存储引擎,包括FAISS、Pinecone、Qdrant等,并且可以轻松地扩展到新的存储引擎。它还提供了丰富的API,使得构建RAG系统变得非常简单。

这里使用Ollama本地部署模型,生产环境可以考虑使用云服务,或者[vLLM](https://github.com/vllm-project/vllm)进行部署。

linux 环境下:

Terminal window
curl -fsSL https://ollama.com/install.sh | sh

Mac 和 Windows 环境下, 可以在官网下载

Milvus 在 Milvus 资源库中提供了 Docker Compose 配置文件。要使用 Docker Compose 安装 Milvus,只需运行

Terminal window
Download the configuration file
wget https://github.com/milvus-io/milvus/releases/download/v2.5.9/milvus-standalone-docker-compose.yml -O docker-compose.yml
Start Milvus
sudo docker compose up -d
Creating milvus-etcd ... done
Creating milvus-minio ... done
Creating milvus-standalone ... done

使用以下命令检查容器是否启动并运行:

Terminal window
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
milvus-etcd quay.io/coreos/etcd:v3.5.18 "etcd -advertise-cli…" etcd 20 minutes ago Up 20 minutes (healthy) 2379-2380/tcp
milvus-minio minio/minio:RELEASE.2023-03-20T20-16-18Z "/usr/bin/docker-ent…" minio 20 minutes ago Up 20 minutes (healthy) 0.0.0.0:9000-9001->9000-9001/tcp, :::9000-9001->9000-9001/tcp
milvus-standalone milvusdb/milvus:v2.5.6 "/tini -- milvus run…" standalone 20 minutes ago Up 20 minutes (healthy) 0.0.0.0:9091->9091/tcp, :::9091->9091/tcp, 0.0.0.0:19530->19530/tcp, :::19530->19530/tcp

uv 使用 Rust 开发的 Python 包和项目管理器。

uv

mac OS 和 linux :

Terminal window
curl -LsSf https://astral.sh/uv/install.sh | sh

uv 管理项目依赖和环境,支持lockfiles, workspaces 等, 类似于 rye 和 poetry。

Terminal window
uv init
uv venv --python=3.10 # 创建项目虚拟环境
source .venv/bin/activate # 激活项目虚拟环境

代码中需要依赖 pymiluvs , llamaindexollma,使用下面的命令安装依赖:

Terminal window
uv add pymilvus
Terminal window
uv add llama-index-vector-stores-milvus
Terminal window
uv add llama-index
Terminal window
uv add llama-index-llms-ollama
Terminal window
uv add llama-index-embeddings-ollama

这里使用的数据是,保存在Github上Paul Graham的一篇文章。

Terminal window
>mkdir -p 'data/paul_graham/'
>wget 'https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt' -O 'data/paul_graham/paul_graham_essay.txt'
from llama_index.core import SimpleDirectoryReader
# load documents
documents = SimpleDirectoryReader(
input_files=["./data/paul_graham_essay.txt"]
).load_data()
print("Document ID:", documents[0].doc_id)

out:

Document ID: 41314907-75fa-4bba-b6d9-6eb36e6add24

这里使用的是的嵌入模型是:modelscope.cn/Embedding-GGUF/gte-Qwen2-1.5B-instruct-Q4_K_M-GGUF:latest , 数据保存到 Milvus 中:

from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.embeddings.ollama import OllamaEmbedding
settings.embed_model = OllamaEmbedding(
model_name="modelscope.cn/Embedding-GGUF/gte-Qwen2-1.5B-instruct-Q4_K_M-GGUF:latest",
base_url="http://127.0.0.1:11434",
)

连接上本地部署的 Milvus 服务:

from llama_index.vector_stores.milvus import MilvusVectorStore
vector_stores = MilvusVectorStore(
collection_name="t_doc",
dim=1536,
uri="http://127.0.0.1:19530",
overwrite=False,
similarity_metric="COSINE",
)
storage_context = StorageContext.from_defaults(vector_store=vector_stores)
index = VectorStoreIndex.from_documents(
docs,
storage_context=storage_context,
show_progress=True,
)

现在我们有了文档,可以创建索引并插入文档。

其中:

  • collection_name: Milvus 集合名称
  • dim: 嵌入向量维度
  • uri: Milvus 服务地址
  • overwrite: 是否覆盖已存在的集合
  • similarity_metric: 相似度度量方法

现在我们已经将文档存储到了索引中,可以针对索引提出问题。

使用的同样的嵌入模型, 从 Milvus 中加载向量,然后创建索引,最后查询数据。

embed_model = OllamaEmbedding(
base_url="http://127.0.0.1:11434",
model_name="modelscope.cn/Embedding-GGUF/gte-Qwen2-1.5B-instruct-Q4_K_M-GGUF:latest",
)
Settings.llm = Ollama(
base_url="http://127.0.0.1:11434",
model="qwen2.5:7b-instruct-q4_K_M",
request_timeout=30,
)
vector_stores = MilvusVectorStore(
collection_name="t_doc",
dim=1536,
uri="http://127.0.0.1:19530",
overwrite=False,
similarity_metric="COSINE",
)
index = VectorStoreIndex.from_vector_store(
vector_store=vector_stores,
embed_model=embed_model,
)
query_engine = index.as_query_engine()
response = query_engine.query("What is AI?")
print(response)
❯ uv run main.py
2025-04-19 15:00:40,435 [DEBUG][_create_connection]: Created new connection using: 3d9484d069f84709bd3befbc643cc588 (async_milvus_client.py:600)
INFO:httpx:HTTP Request: POST http://127.0.0.1:11434/api/embeddings "HTTP/1.1 200 OK"
INFO:httpx:HTTP Request: POST http://127.0.0.1:11434/api/chat "HTTP/1.1 200 OK"
AI, or Artificial Intelligence, in the early 1980s as described, involved creating programs that could understand natural language to a certain extent. The speaker believed at first that AI was about teaching programs like SHRDLU more words and expanding their formal representations of concepts. However, he later realized that this approach had significant limitations because there was an unbridgeable gap between the subset of natural language these early programs could handle and true human-like understanding. He came to see AI as a field with potential but one that was fundamentally flawed in its initial approaches, particularly those relying on explicit data structures to represent concepts without achieving actual intelligence.

在博客原文中的相关SHRDLU:

Terminal window
rg SHRDLU
data/paul_graham/paul_graham_essay.txt
25:AI was in the air in the mid 1980s, but there were two things especially that made me want to work on it: a novel by Heinlein called The Moon is a Harsh Mistress, which featured an intelligent computer called Mike, and a PBS documentary that showed Terry Winograd using SHRDLU. I haven't tried rereading The Moon is a Harsh Mistress, so I don't know how well it has aged, but when I read it I was drawn entirely into its world. It seemed only a matter of time before we'd have Mike, and when I saw Winograd using SHRDLU, it seemed like that time would be a few years at most. All you had to do was teach SHRDLU more words.
29:For my undergraduate thesis, I reverse-engineered SHRDLU. My God did I love working on that program. It was a pleasing bit of code, but what made it even more exciting was my belief — hard to imagine now, but not unique in 1985 — that it was already climbing the lower slopes of intelligence.
33:I applied to 3 grad schools: MIT and Yale, which were renowned for AI at the time, and Harvard, which I'd visited because Rich Draves went there, and was also home to Bill Woods, who'd invented the type of parser I used in my SHRDLU clone. Only Harvard accepted me, so that was where I went.
37:What these programs really showed was that there's a subset of natural language that's a formal language. But a very proper subset. It was clear that there was an unbridgeable gap between what they could do and actually understanding natural language. It was not, in fact, simply a matter of teaching SHRDLU more words. That whole way of doing AI, with explicit data structures representing concepts, was not going to work. Its brokenness did, as so often happens, generate a lot of opportunities to write papers about various band-aids that could be applied to it, but it was never going to get us Mike.

当使用 QueryEngine 时,LlamaIndex 内部会使用 Prompt 来指导 LLM 如何利用检索到的信息来回答用户的查询。

自定义流程中: 可以获取检索结果,然后自己构建 Prompt 调用 LLM 进行进一步处理。

使用 Retriever 获取 Top-K,然后自定义 Prompt 进行 LLM 验证

Section titled “使用 Retriever 获取 Top-K,然后自定义 Prompt 进行 LLM 验证”

这种方法更灵活,控制力更强,专注于“识别关联”而非“生成答案”。

  1. 使用 Retriever 获取节点:
retriever = index.as_retriever(similarity_top_k=5) # 获取最相似的 5 个
question_text = "What is AI"
retrieved_nodes = retriever.retrieve(question_text)
  1. 构建自定义 Prompt 调用 LLM:

目标: 让 LLM 从检索到的 retrieved_nodes 中选出答案。 Prompt 示例:

from llama_index.llms.ollama import Ollama
llm = Ollama(
base_url="http://127.0.0.1:11434",
model="qwen2.5:7b-instruct-q4_K_M",
request_timeout=30,
)
retriever = index.as_retriever(similarity_top_k=5) # 获取最相似的 5 个
question_text = "What is AI"
retrieved_nodes = retriever.retrieve(question_text)
context_str = "\n\n".join([f"{node.text}" for node in retrieved_nodes])
prompt_template = f"""
"{question_text}"
Context:
---
{context_str}
---
"""
response = Settings.llm.complete(prompt_template)
print(response.text)