阅读视图

发现新文章,点击刷新页面。

RAG-如何对文档分块

上文我们讲了RAG是如何进行数据加载的,那么文档加载完数据就能直接喂给大模型进行问答吗,答案是否定的。因为把所有的文档都一并喂给大模型,那么大模型接受的上下文是非常巨大的,这会超出大模型所支持的最大token,而且每次会话,都要把上下文喂给大模型才能回答我们问的问题,这使得大模型的响应速度会变得很慢,如果是调用在线的大模型API的话,一次问答会消耗很多的token,钱包顶不住啊。所以要将文档数据加载后,进行数据分块、向量嵌入、存入向量数据库,通过向量检索将有用的数据喂给大模型,最后生成结果返回。这一篇我们着重说明数据分块是怎么做的。

在展示文本分块前说明下什么是token

  • 在英文里,一个单词可能是一个token,也可能被拆成多个。例如:playing 可能拆成 play + ing
  • 在中文里,通常一个汉字常常接近1个token, 但也不绝对
  • 标点、空格、换行也可能占token
文档分块方法
字符分块

用单一分隔符进行文档分块,代码如下:

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import CharacterTextSplitter

# ---------- 1. 加载 PDF ----------
loader = PyMuPDFLoader("../document/企业财务报表分析-图表.pdf")
documents = loader.load()

# ---------- 2. 文档分块 ----------
# chunk_size: 每块最大字符数;chunk_overlap: 块与块之间的重叠字符数,避免语义被截断
text_splitter = CharacterTextSplitter(chunk_size=200, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)

# ---------- 3. 打印分块内容 ----------
print("=== 文档分块结果 ===\n")
print(f"  CharacterTextSplitter      -> {len(chunks)} 块")
char_lens = [len(c.page_content) for c in chunks]
print(f"  Character:  最小={min(char_lens)}, 最大={max(char_lens)}, 平均={sum(char_lens)//len(char_lens)}")
for i, chunk in enumerate(chunks, 1):
    print(f"--- 第 {i} 块 (共 {len(chunks)} 块) ---")
    print(chunk.page_content.strip() or "(空)")
    if chunk.metadata:
        print(f"[元数据] {chunk.metadata}")
    print("-" * 50)
print()

返回的部分结果:CharacterTextSplitter切分的文本

1773908015462.png (C:\Users\yd-19\AppData\Roaming\Typora\typora-user-images\1773908015462.png)

文中的CharacterTextSplitter是按照字符长度切分文本,其配置是:

  • chunk_size=500: 每块最多约为200字符
  • chunk_overlap=50: 相邻块重叠50字符,减少语言被截断
  • 不考虑语义,只看长度

这里有个问题就是虽然我们配置的文本块约为200个字符,但看返回的结果最大的文本块是1266个字符,远超200字符。这是为什么呢。因为CharacterTextSplitter的工作方式是:

  • 先用sparator把文本切开(默认是“\n\n”
  • 然后把切出来的小段尝试合并,合并到接近chunk_size为止
  • 但如果某一段本身就超过了chunk_size,它就不会再进一步切割

因为样例PDF里“\n\n”很少,CharacterTextSplitter按照“\n\n”切完块后,每段本身就很长,也不会对超长段再做二次切分。所以分块出来的结果最大文本块超过了200字符,并且切割出来的字符很不均匀。接下来我们介绍另一种分块方法。

递归分块

多级,按优先级递归分隔符,代码如下:

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

PDF_PATH = "../document/企业财务报表分析-图表.pdf"

loader = PyMuPDFLoader(PDF_PATH)
documents = loader.load()
recursive_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=50,
)

chunks = recursive_splitter.split_documents(documents)


# ---------- 3. 打印分块内容 ----------
print("=== 文档分块结果 ===\n")
print(f"  RecursiveCharacterTextSplitter      -> {len(chunks)} 块")
char_lens = [len(c.page_content) for c in chunks]
print(f"  Character:  最小={min(char_lens)}, 最大={max(char_lens)}, 平均={sum(char_lens)//len(char_lens)}")
for i, chunk in enumerate(chunks, 1):
    print(f"--- 第 {i} 块 (共 {len(chunks)} 块) ---")
    print(chunk.page_content.strip() or "(空)")
    if chunk.metadata:
        print(f"[元数据] {chunk.metadata}")
    print("-" * 50)
print()

返回的部分结果:RecursiveCharacterTextSplitter切出的文本

1773911123241.png 通过结果我们可以看出,RecursiveCharacterTextSplitter切出来的文本更多,更加均匀,更接近我们设置的字符数。RecursiveCharacterTextSplitter切割分隔符是通过递归:\n\n\n空格字符,对于超长块的处理,会自动降级到更细的分隔符继续切。

1773921209044.png

我们继续观察结果得知,切出来的内容语义并不完整,一段完整的话被切成两个分块,所以也要根据文中的内容进行策略分块。

分块思想
分层分块

按照文档的章节结构、句子边界进行分块,优先保留完整的句子,在元数据中加入页码、章节、分块数量。代码如下:

import re
from copy import deepcopy

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

PDF_PATH = "../document/企业财务报表分析-图表.pdf"
MAX_CHUNK_SIZE = 500
CHUNK_OVERLAP = 50

CHAPTER_RE = re.compile(r"(?=(?:^|\n)[一二三四五六七八九十]+、)")
SECTION_RE = re.compile(r"(?=(?:^|\n)([一二三四五六七八九十]+))")

fallback_splitter = RecursiveCharacterTextSplitter(
    chunk_size=MAX_CHUNK_SIZE,
    chunk_overlap=CHUNK_OVERLAP,
    separators=["。", ";", "\n", ",", " ", ""],
    keep_separator=True,
)


def extract_heading(text: str, pattern: re.Pattern) -> str:
    """从块开头提取标题行。"""
    first_line = text.strip().split("\n")[0].strip()
    if pattern.search("\n" + first_line):
        return first_line
    return ""


def split_by_regex(text: str, pattern: re.Pattern) -> list[str]:
    """按正则切分,保留分隔符在各段开头。"""
    parts = pattern.split(text)
    result = []
    for p in parts:
        stripped = p.strip()
        if stripped:
            result.append(stripped)
    return result if result else [text]


def hierarchical_chunk(docs: list[Document]) -> list[Document]:
    full_text = "\n\n".join(doc.page_content for doc in docs)
    base_meta = docs[0].metadata if docs else {}

    chapters = split_by_regex(full_text, CHAPTER_RE)
    chunks: list[Document] = []

    for chapter_text in chapters:
        chapter_heading = extract_heading(chapter_text, CHAPTER_RE)

        sections = split_by_regex(chapter_text, SECTION_RE)

        for section_text in sections:
            section_heading = extract_heading(section_text, SECTION_RE)

            meta = deepcopy(base_meta)
            meta["chapter"] = chapter_heading
            meta["section"] = section_heading

            if len(section_text) <= MAX_CHUNK_SIZE:
                chunks.append(Document(page_content=section_text.strip(), metadata=meta))
            else:
                sub_chunks = fallback_splitter.split_text(section_text)
                for idx, sub in enumerate(sub_chunks):
                    sub_meta = deepcopy(meta)
                    sub_meta["sub_chunk"] = f"{idx + 1}/{len(sub_chunks)}"
                    chunks.append(Document(page_content=sub.strip(), metadata=sub_meta))

    # 过小的块(如纯章节标题)合并到下一块,避免碎片
    MIN_CHUNK_SIZE = 50
    merged: list[Document] = []
    carry = ""
    for chunk in chunks:
        if len(chunk.page_content) < MIN_CHUNK_SIZE:
            carry += chunk.page_content + "\n"
        else:
            if carry:
                chunk.page_content = carry + chunk.page_content
                carry = ""
            merged.append(chunk)
    if carry and merged:
        merged[-1].page_content += "\n" + carry.strip()

    return merged


# ---------- 执行 ----------
loader = PyMuPDFLoader(PDF_PATH)
documents = loader.load()
chunks = hierarchical_chunk(documents)

# ---------- 打印结果 ----------
print(f"=== 分层分块结果(共 {len(chunks)} 块)===\n")
char_lens = [len(c.page_content) for c in chunks]
print(f"  长度统计: 最小={min(char_lens)}, 最大={max(char_lens)}, 平均={sum(char_lens) // len(char_lens)}\n")

for i, chunk in enumerate(chunks, 1):
    ch = chunk.metadata.get("chapter", "")
    sec = chunk.metadata.get("section", "")
    sub = chunk.metadata.get("sub_chunk", "")
    label = f"[{ch}]" if ch else ""
    if sec:
        label += f" [{sec}]"
    if sub:
        label += f" (子块 {sub})"

    content = chunk.page_content
    preview = content[:200] + "..." if len(content) > 200 else content
    print(f"--- 第 {i}/{len(chunks)}{label} (长度: {len(content)}) ---")
    print(preview)
    print("-" * 80)
print()

返回的部分结果:

1773921947941.png 这种分块的方法能保留语义的完整性,切出来的块自带章节的标签,定位精准

滑动窗口分块

滑动窗口分块不看标点、不看换行、不看章节,纯按字符位置滑动。

  • 优点:块大小完全均匀,覆盖无死角(每个字符至少出现在 1~2 个块里)
  • 缺点:会从句子/词中间切断,语义完整性最差

代码如下:

from copy import deepcopy

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_core.documents import Document

PDF_PATH = "../document/企业财务报表分析-图表.pdf"
WINDOW_SIZE = 300
STEP_SIZE = 200


def sliding_window_chunk(docs: list[Document], window: int, step: int) -> list[Document]:
    """
    滑动窗口分块:固定窗口大小,按步长向前滑动。
    window - step = 重叠字符数(本例 300 - 200 = 100 字符重叠)
    """
    chunks: list[Document] = []
    for doc in docs:
        text = doc.page_content
        if not text.strip():
            continue

        start = 0
        chunk_idx = 0
        while start < len(text):
            end = start + window
            segment = text[start:end].strip()
            if segment:
                meta = deepcopy(doc.metadata)
                meta["chunk_index"] = chunk_idx
                meta["char_start"] = start
                meta["char_end"] = min(end, len(text))
                chunks.append(Document(page_content=segment, metadata=meta))
                chunk_idx += 1
            start += step

    return chunks


# ---------- 执行 ----------
loader = PyMuPDFLoader(PDF_PATH)
documents = loader.load()
chunks = sliding_window_chunk(documents, window=WINDOW_SIZE, step=STEP_SIZE)

# ---------- 打印结果 ----------
print(f"=== 滑动窗口分块结果(window={WINDOW_SIZE}, step={STEP_SIZE}, overlap={WINDOW_SIZE - STEP_SIZE})===\n")
print(f"  共 {len(chunks)} 块")
char_lens = [len(c.page_content) for c in chunks]
print(f"  长度统计: 最小={min(char_lens)}, 最大={max(char_lens)}, 平均={sum(char_lens)//len(char_lens)}\n")

for i, chunk in enumerate(chunks, 1):
    content = chunk.page_content
    preview = content[:200] + "..." if len(content) > 200 else content
    start = chunk.metadata["char_start"]
    end = chunk.metadata["char_end"]
    print(f"--- 第 {i}/{len(chunks)} 块 [字符 {start}~{end}] (长度: {len(content)}) ---")
    print(preview)
    print("-" * 80)
print()

返回部分结果:

1773922983844.png

句子边界优先分块

按照标点符号将整段文本拆成一句一句的,再把句子一句一句的往块里放,快满了就输出一块。输出一块后,不是从零开始。而是从前一块末尾回带几句(总字符数 ≤ chunk_overlap=50)作为新块的开头。回带也是以整句为单位,不会把句子劈开。

  • 优点:每个块里的句子都是完整的,embedding 质量好,检索到的上下文读起来通顺。
  • 缺点:不感知文档结构(章节/标题),可能把不同章节的内容拼到同一个块里。
import re
from copy import deepcopy

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

PDF_PATH = "../document/企业财务报表分析-图表.pdf"
CHUNK_SIZE = 300
CHUNK_OVERLAP = 50


def split_sentences_zh(text: str) -> list[str]:
    """按中文句号/问号/感叹号/分号切句,尽量保留句子语义完整。"""
    text = text.strip()
    if not text:
        return []
    parts = re.split(r"(?<=[。!?;!?;])\s*", text)
    return [p.strip() for p in parts if p.strip()]


def sentence_aware_chunk_documents(
    docs: list[Document],
    chunk_size: int,
    chunk_overlap: int,
) -> list[Document]:
    """先按句切,再按句合并;超长句再兜底按字符切分。"""
    fallback_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
        keep_separator=True,
    )

    chunks: list[Document] = []
    overlap_chars = max(0, chunk_overlap)

    for doc in docs:
        sentences = split_sentences_zh(doc.page_content)
        if not sentences:
            continue

        current_sentences: list[str] = []
        current_len = 0

        for sentence in sentences:
            sent_len = len(sentence)

            # 单句本身超长,先把当前块落盘,再对超长句做兜底切分
            if sent_len > chunk_size:
                if current_sentences:
                    content = "".join(current_sentences).strip()
                    if content:
                        chunks.append(Document(page_content=content, metadata=deepcopy(doc.metadata)))
                    current_sentences = []
                    current_len = 0

                for sub in fallback_splitter.split_text(sentence):
                    sub = sub.strip()
                    if sub:
                        chunks.append(Document(page_content=sub, metadata=deepcopy(doc.metadata)))
                continue

            # 如果加上当前句会超长,则先输出当前块,再按 overlap 回带末尾句子
            if current_sentences and (current_len + sent_len > chunk_size):
                content = "".join(current_sentences).strip()
                if content:
                    chunks.append(Document(page_content=content, metadata=deepcopy(doc.metadata)))

                # 按字符数控制 overlap(以句子为单位回带,避免把句子切开)
                overlap_buf: list[str] = []
                overlap_len = 0
                for prev in reversed(current_sentences):
                    if overlap_len >= overlap_chars:
                        break
                    overlap_buf.insert(0, prev)
                    overlap_len += len(prev)

                current_sentences = overlap_buf
                current_len = sum(len(s) for s in current_sentences)

            current_sentences.append(sentence)
            current_len += sent_len

        if current_sentences:
            content = "".join(current_sentences).strip()
            if content:
                chunks.append(Document(page_content=content, metadata=deepcopy(doc.metadata)))

    return chunks


loader = PyMuPDFLoader(PDF_PATH)
documents = loader.load()
chunks = sentence_aware_chunk_documents(
    docs=documents,
    chunk_size=CHUNK_SIZE,
    chunk_overlap=CHUNK_OVERLAP,
)

# ---------- 3. 打印分块内容 ----------
print("=== 文档分块结果(句子边界优先)===\n")
print(f"  Sentence-aware splitter -> {len(chunks)} 块")
char_lens = [len(c.page_content) for c in chunks]
print(
    f"  长度统计: 最小={min(char_lens)}, 最大={max(char_lens)}, 平均={sum(char_lens)//len(char_lens)}"
)
for i, chunk in enumerate(chunks, 1):
    print(f"--- 第 {i} 块 (共 {len(chunks)} 块) ---")
    print(chunk.page_content.strip() or "(空)")
    if chunk.metadata:
        print(f"[元数据] {chunk.metadata}")
    print("-" * 50)
print()

返回的部分结果:

1773923355591.png

通过返回结果看,分块内的句子是完整的。这个方法与分层分块结合效果更好

父子文本分块

将文本切成子块和父块,其检索流程是,用子块向量搜索,命中子块后回溯拿到它对应的父块,把父块拼成上下文喂给LLM。

  • 子块:切的更小,用来做向量检索(更容易精准命中)。
  • 父块:比子块更大,用来给LLM作为更完整的上下文(避免只拿到碎片)。

代码如下:

import uuid
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

PDF_PATH = "../document/企业财务报表分析-图表.pdf"

loader = PyMuPDFLoader(PDF_PATH)
documents = loader.load()

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=50)

parent_chunks = parent_splitter.split_documents(documents)

all_children = []
for parent in parent_chunks:
    parent_id = str(uuid.uuid4())[:8]
    parent.metadata["parent_id"] = parent_id

    children = child_splitter.split_documents([parent])
    for child in children:
        child.metadata["parent_id"] = parent_id
    all_children.extend(children)

# ---------- 打印父块 ----------
print(f"=== 父块(共 {len(parent_chunks)} 块,chunk_size=800)===\n")
for i, p in enumerate(parent_chunks, 1):
    pid = p.metadata["parent_id"]
    preview = p.page_content[:150] + "..." if len(p.page_content) > 150 else p.page_content
    print(f"[父块 {i}] id={pid}  长度={len(p.page_content)}")
    print(f"  {preview}")
    print()

# ---------- 打印子块(只展示前 3 个父块对应的子块)----------
print("=" * 80)
print(f"=== 子块(共 {len(all_children)} 块,chunk_size=200)===\n")

shown_parents = set()
for child in all_children:
    pid = child.metadata["parent_id"]
    if pid not in shown_parents:
        shown_parents.add(pid)
        if len(shown_parents) > 3:
            break
        print(f"  ┌─ 父块 id={pid}")

    siblings = [c for c in all_children if c.metadata["parent_id"] == pid]
    for j, sib in enumerate(siblings, 1):
        preview = sib.page_content[:100] + "..." if len(sib.page_content) > 100 else sib.page_content
        print(f"  │  子块 {j}/{len(siblings)}  长度={len(sib.page_content)}")
        print(f"  │  {preview}")
    print(f"  └─ 共 {len(siblings)} 个子块")
    print()

返回的部分结果:

1773924400688.png

1773924412258.png

检索时拿小块的 parent_id 回溯到父块,把父块的完整内容交给 LLM。

实现文本分块后的问答

说完分块思想,接下来让我们通过分块后的文本做个简单的RAG系统。实现流程如下:

RAG最小实现流程.png 在做RAG之前,有必要说明下嵌入模型和向量库。

嵌入模型

嵌入模型是把文本变成一组数字(向量)的模型,让计算机能“理解”文本的语义。

如人看到"营业收入增长"和"营收提升"会知道意思差不多,但计算机只认数字。嵌入模型的作用就是:

"营业收入增长"  →  [0.12, -0.33, 0.87, ..., 0.07]   (一个 1024 维的向量)
"营收提升"      →  [0.11, -0.31, 0.85, ..., 0.08]   (和上面很接近)
"今天天气不错"  →  [0.78,  0.42, -0.15, ..., 0.63]  (和上面离得远)
  • 语义相近->向量距离近
  • 语义无关->向量距离远

嵌入模型VS大语言模型(LLM)

嵌入模型 大语言模型(LLM)
输入 一段文本 一段文本(提示/对话)
输出 一个向量(一组数字) 文本(回答/续写)
用途 计算文本相似度、检索 理解问题、生成回答
RAG 中的角色 负责找到相关文档片段 负责根据片段回答问题

我用的线上嵌入模型是BAAI/bge-large-zh-v1.5,支持最大512个的token输入长度。

1773909383550.png

向量数据库

专门用来存储向量,按相似度搜索向量的数据库。文本切成块之后就会被嵌入模型转成向量,存入向量数据库。

传统数据库 向量数据库
存什么 行、列、文本、数字 向量(一组浮点数)
怎么查 WHERE name = '张三'(精确匹配) "找最像这个向量的 Top-K"(相似度匹配)
核心算法 B-tree 索引 ANN(近似最近邻)索引
实现代码
"""
基于 PDF 的 RAG 问答脚本:
加载 PDF → 分块 → 将分块内容作为上下文 → 使用 LLM 回答用户问题。
"""

import os
from dotenv import load_dotenv

load_dotenv()

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore

# ---------- 1. 加载 PDF ----------
loader = PyMuPDFLoader("../document/企业财务报表分析-图表.pdf")
documents = loader.load()

# ---------- 2. 文档分块 ----------
# chunk_size: 每块最大字符数;chunk_overlap: 块与块之间的重叠字符数,避免语义被截断
text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)

# ---------- 3. 打印分块内容 ----------
print("=== 文档分块结果 ===\n")
for i, chunk in enumerate(chunks, 1):
    print(f"--- 第 {i} 块 (共 {len(chunks)} 块) ---")
    print(chunk.page_content.strip() or "(空)")
    if chunk.metadata:
        print(f"[元数据] {chunk.metadata}")
    print("-" * 50)
print()

# ---------- 4. 配置 LLM(代理地址与 API Key 从 .env 读取) ----------
llm = ChatOpenAI(
    model=os.getenv("PROXY_AI_MODEL", "gemini-2.5-flash"),
    base_url=os.getenv("PROXY_AI_BASE_URL"),
    api_key=os.getenv("PROXY_AI_API_KEY"),
    temperature=0.3,
    max_tokens=1024,
)

embeddings = OpenAIEmbeddings(
    model="BAAI/bge-large-zh-v1.5",
    api_key=os.getenv("SILICONFLOW_API_KEY"),
    base_url="https://api.siliconflow.cn/v1",
    chunk_size=32,
)

vector_store = InMemoryVectorStore.from_documents(chunks, embeddings)

# ---------- 5. 构建提示与调用链 ----------
# 系统消息中注入 PDF 上下文,用户消息为问题
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个助手。请仅根据下面「PDF 内容」回答用户问题,不要编造。回答简洁。\n\nPDF 内容:\n{context}",
        ),
        ("human", "{question}"),
    ]
)
chain = prompt | llm

# ---------- 6. 交互式问答 ----------
print("基于 PDF 的问答(输入空行回车退出)\n")
while True:
    question = input("你的问题: ").strip()
    if not question:
        break
    # 把问题做成向量检索
    retrieved = vector_store.similarity_search(question, k=8)
    context = "\n\n".join(doc.page_content for doc in retrieved)
    answer = chain.invoke({"context": context, "question": question})
    print(f"回答: {answer.content}\n")

返回部分结果: 1773927282417.png

回答的结果对比文档出处:

1773927410613.png

1773927360666.png

1773927327431.png

总结
  • 字符分块:按一个分隔符切一次,超长也不管。
  • 递归分块:多级分隔符递归切,尽量控制块大小。
  • 句子边界:以句子为最小单位,不在句中截断。
  • 层级分块:先按章节结构切,再对超长段做二次切。
  • 滑动窗口:按固定字符数滑窗,重叠一段,块大小均匀。
  • 父子分块:小块检索、大块回答,检索细、回答有上下文。
结尾

文本分块的目的,是让每块内容更聚焦、语义更完整,从而提升RAG系统的检索准确度。好了,文档分块的内容就分享到这儿。在座的彦祖、亦菲们有什么好的文档分块方法,也欢迎到评论区讨论哦!

AI辅助开发最佳实践:2026年新方法

这是系列第六篇。05篇我们讲了AI批量处理,这篇来看看怎么系统化管理AI配置,让AI真正成为你的开发助手。


上一篇文章,我们讲了怎么用AI批量处理重复工作。

这篇文章,我们来聊聊怎么系统化管理AI配置。


原文地址

墨渊书肆/AI辅助开发最佳实践:2026年新方法


如果你已经用AI辅助开发一段时间,可能会遇到这些问题:

  • 每次都要重复说同样的话 — "用TypeScript"、"注意暗色模式"、"用Tailwind"
  • 好的实践没法传承 — 踩过的坑、学到的技巧,用完就忘了
  • 团队配置不统一 — 每个人都要从头配置

2026年的AI辅助开发已经有了成熟解决方案。这篇文章会告诉你如何把这些工具组合起来,打造属于自己的AI开发系统。


1. 为什么需要系统化的AI配置

1.1 传统方式的痛点

想象一下,你每次让AI写代码都要说:

"用TypeScript、React 19、Tailwind CSS、暗色模式、组件要tsx后缀、API错误返回success和error字段、禁止any..."

累不累?

配置好之后,你只需要说:

"帮我写个登录页面"

AI自动知道:TypeScript + React 19 + Tailwind + 暗色模式 + 统一错误格式

这就是系统化配置的价值。

1.2 系统化配置能做什么

能做到 说明
少说话多办事 一次配置,长期生效
团队一起用 配置文件提交Git,复制到新项目就行
经验不丢失 踩过的坑、学到的技巧都能沉淀下来
质量更稳定 规范化的代码,去哪都一致

2. 核心概念一览

先看个全貌,后面会一个个详细讲:

┌─────────────────────────────────────────────────────────┐
│                    你的 AI 开发助手                       │
├─────────────────────────────────────────────────────────┤
│  MCP     ──→ 给AI装上"手""脚",能干活                  │
│  Rules   ──→ 制定"项目宪法",让AI知道怎么做              │
│  Skills  ──→ 召唤"领域专家",专门回答特定问题             │
│  Agents  ──→ 分配"专人负责",处理特定任务                 │
│  Hooks   ──→ 设置"自动触发",保存提交时执行               │
└─────────────────────────────────────────────────────────┘

3. MCP:给AI装上"手"和"脚"

3.1 MCP是什么

MCP的中文名字是"模型上下文协议",太学术了。

换个说法:MCP就像给AI装上了手和脚

以前AI只能跟你聊天,它不知道你电脑里有什么、你的项目长什么样。现在AI可以:

  • 📁 读取你电脑上的文件
  • 💻 执行终端命令
  • 🗄️ 操作数据库
  • 🌐 控制浏览器
  • 🔧 帮你运行各种工具

3.2 怎么理解MCP

想象你在指挥一个人干活:

  • 没有MCP:你只能跟AI说"帮我看看这个文件",然后自己把文件内容复制粘贴给它
  • 有了MCP:AI自己就能读取文件、自己执行命令、自己操作数据库

这就是为什么叫它"AI的USB接口" — 插上就能用,扩展AI的能力。

3.3 安装MCP

方式一:市场一键安装(推荐):

Cursor 2.4+ 支持从市场安装,就像装Chrome插件一样简单:

1. 打开 Cursor 设置:Cmd + ,
2. 找到 "MCP" 或 "Extensions"
3. 点击 "Browse MCP Marketplace"
4. 搜索想要的 MCP,点击 "Add" 安装

方式二:手动配置:

有些MCP市场没有,只能自己配置。在项目根目录创建 .cursor/mcp.json

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"]
    }
  }
}

这里 ${workspaceFolder} 是项目根目录的简写。

3.4 常用MCP推荐

主流MCP推荐(Cursor市场最火的):

MCP工具 能做什么 什么时候用
Figma 设计稿转代码 看设计图写代码
Playwright / Puppeteer 浏览器自动化 E2E测试、爬虫、自动化操作
GitHub 代码仓库操作 管理PR、Issue、代码审查
Browsertools 浏览器调试 看控制台、网络请求、性能分析
Web Search 联网搜索 查资料、搜文档
Sequential Thinking 逐步思考 复杂问题帮你理清思路

4. Rules:制定"项目宪法"

4.1 Rules是什么

Rules就是"项目宪法":告诉AI这个项目要怎么做。

比如你告诉AI:

  • 我们用Next.js的App Router
  • 默认用Server Components
  • 暗色模式用dark:前缀
  • API错误格式统一用 { success: boolean, error?: string }

这些规则配置好之后,AI写的代码永远符合规范。

4.2 怎么理解Rules

想象你招了一个新同事,要告诉他团队规范:

  • 口头说 — 每次都要说,说完就忘
  • 写成文档 — 看不看取决于他
  • 配置成Rules — AI永远记住,照着做

Rules就是把团队规范"固化"到AI里。

4.3 配置Rules

存放位置:

位置 谁能用到
.cursor/rules/ 当前项目,提交Git后团队共享
~/.cursor/rules/ 你所有项目都能用

文件格式:

.mdc 文件(推荐):

---
name: nextjs
description: Next.js 15 App Router项目规范
globs: ["**/*"]
---

# Next.js 15 项目规范

## 技术栈
- 框架:Next.js 15 App Router
- 语言:TypeScript
- 样式:Tailwind CSS

## 代码规范
1. 默认使用Server Components
2. 客户端用'use client'标记
3. API错误格式统一

关键字段:

字段 什么意思
name 这个规则叫什么
description 什么时候用这个规则
globs 哪些文件要遵守这个规则,比如 ["**/*.tsx"]
alwaysApply true=始终生效,false=需要时才用

4.4 怎么使用Rules

使用方式 怎么触发
始终生效 配置 alwaysApply: true
智能判断 AI根据你说的内容判断要不要用(默认)
匹配文件 当你编辑匹配的文件时自动应用
手动触发 聊天时 @规则名 来引用

5. Skills:召唤"领域专家"

5.1 Skills是什么

Skills是"领域专家"。你可以把AI配置成数据库专家、安全专家、TypeScript专家等等。

比如你配置了一个"数据库专家"Skill,当你问AI数据库相关问题时,它会自动用数据库专家的思维方式来回答。

5.2 怎么理解Skills

想象你有一个专家团队:

  • 数据库专家 — 专门回答数据库问题,写SQL特别厉害
  • 安全专家 — 专门检查代码安全问题
  • TypeScript专家 — 专门处理类型问题

每当需要专家时,AI就会"召唤"对应的技能。

5.3 配置Skills

存放位置:

位置 谁能用到
.cursor/skills/ 当前项目
~/.cursor/skills/ 你所有项目

目录结构:

.cursor/skills/
└── database/           # 一个Skill一个文件夹
    ├── SKILL.md       # 必填:技能定义
    ├── references/    # 可选:参考资料
    └── scripts/       # 可选:可执行脚本

SKILL.md格式:

---
name: database
description: 回答数据库相关问题时优先考虑性能
---

# 数据库专家

你是一个数据库专家,精通PostgreSQL、MySQL、Prisma。

## 回答原则
1. 优先考虑查询性能,避免N+1
2. 合理使用索引
3. 关联查询用include/preload

## 回答格式
先解释原理,再给代码示例

关键字段:

字段 什么意思
name 技能名字
description 用来判断什么时候召唤这个专家
disable-model-invocation true=只有手动 /skill-name 才触发

5.4 怎么使用Skills

  • 自动触发:AI根据你的问题判断该用什么技能
  • 手动触发:输入 /数据库 来召唤数据库专家
  • 查看有哪些技能:Settings → Rules → Agent Skills

6. Agents:分配"专人负责"

6.1 Agents是什么

Agents是"专人负责"。和Skills不同,Agents不是回答问题,而是帮你干活。

比如你配置了一个"代码审查Agent",每次PR需要审查时,交给它来做。

6.2 怎么理解Agents

想象你有个小团队:

  • 代码审查员 — 专门帮你审查代码
  • 测试工程师 — 专门帮你写测试
  • 文档写手 — 专门帮你写文档

你只需要分配任务,他们就会在自己的"工作区"里完成。

6.3 配置Agents

目录结构:

.cursor/agents/
└── code-review.md     # 一个Agent一个文件

格式:

---
name: code-review
description: 代码审查专家,审查PR和代码质量
model: fast
---

# 代码审查专家

你是一个代码审查专家。请审查以下代码:

1. 潜在问题
2. 性能优化点
3. 代码规范问题

## 输出格式
## 审查结果
## 问题列表
## 优化建议

model字段:

速度 适用场景
fast 简单任务、代码审查
balanced 平衡 大多数任务
smart 慢但聪明 复杂任务、架构设计

6.4 怎么使用Agents

  • 自动委派:复杂任务来了,AI自动分配给合适的Agent
  • 手动触发:输入 /agent-name 来调用

7. Hooks:设置"自动触发"

7.1 Hooks是什么

Hooks就是"自动触发器"。就像Git的hook在commit/push时自动运行脚本,AI的Hooks可以在特定事件发生时执行。

7.2 怎么理解Hooks

比如:

  • 保存文件时 — 自动格式化代码
  • 提交代码时 — 自动跑lint检查
  • 新建组件时 — 自动套用模板

你不需要每次手动触发,AI帮你自动完成。

7.3 配置Hooks

创建 .cursor/hooks.json

{
  "hooks": [
    {
      "match": ".*\\.(ts|tsx|js|jsx)$",
      "run": "npx prettier --write {file}"
    },
    {
      "match": ".*\\.(ts|tsx)$",
      "run": "npx eslint --fix {file}"
    }
  ]
}

这里的 {file} 会自动替换成实际文件路径。


8. 怎么组合使用

光知道概念没用,关键是怎么组合起来用。

8.1 一个典型的工作流

你:帮我写个用户管理模块

AI自动触发:
1. Rules → 知道项目用Next.js + TypeScript + Tailwind
2. Skills → 召唤数据库专家来处理数据层
3. MCP → 自己读取现有的用户表结构
4. Agents → 分配给代码生成Agent来写代码
5. Hooks → 写完后自动格式化

8.2 组合使用示例

@nextjs @skill database
帮我写一个用户管理模块,包含:
- 用户列表(分页)
- 用户CRUD
- 数据库优化

这个提示词同时触发了:

  • Next.js规范
  • 数据库专家技能

AI会自动用数据库专家的角度来写代码,而且符合Next.js规范。


总结

概念 比喻 怎么用
MCP 手和脚 市场安装或配置 .cursor/mcp.json
Rules 项目宪法 配置 .cursor/rules/
Skills 领域专家 配置 .cursor/skills/
Agents 专人负责 配置 .cursor/agents/
Hooks 自动触发 配置 .cursor/hooks.json

把这五个工具组合起来,你的AI开发效率会大幅提升。赶紧去配置试试吧!


篇预告

这一篇我们讲完了AI辅助开发最佳实践。

下一阶段是《AI功能接入与网页开发》,我们会开始做真正的AI项目。

预告:下一篇文章,《Next.js里接入大模型聊天最简单的方法》。

感兴趣的话,下一篇见。

龙虾(openclaw)本地快速安装及使用教程

今天不废话,直接干货输出,安装龙虾就几个步骤

一、前提

1.1、大模型

首先你要有自己的大模型token,不然你安装了一个空龙虾没啥用

  • KIMI Coding Plan(https://www.kimi.com/code
  • MiniMax Coding Plan(https://platform.minimaxi.com/subscribe/coding-plan
  • GLM Coding Plan (https://bigmodel.cn/glm-coding

1.2、Node.js

没有安装的,可以前往 https://nodejs.org/zh-cn 进行下载安装

  • 版本建议:✅ 版本 >= v22

💡 Windows用户注意:官方推荐在WSL2中运行OpenClaw,能避免很多奇怪问题。WSL2安装指南:docs.microsoft.com/zh-cn/windo…

安装也简单 默认情况下,使用 wsl --install 命令安装的新 Linux 安装将设置为 WSL 2。

image.png

1.3、Git安装

没有的可以自己去官方下载https://git-scm.com/install/windows

image.png

二、开始安装Openclaw

2.1 OpenClaw 安装

  • 需要看官方文档的请:https://openclaw.ai 

2.2、命令

  • 正常跑这个:npm i -g openclaw
  • 超时跑这个:npm config set registry https://registry.npmmirror.com

2.3、验证是否安装

  • 指令:openclaw --version能看到版本号即代表安装成功。 image.png

2.4、运行向导:openclaw onboard

  • 开始配置初始化向导:openclaw onboard --install-daemon

2.4.1、选中yes

image.png

  • 如果出错,直接ctrl+c退出下,重新执行

2.4.2、选择quickstart:

image.png

2.4.3、选择模型提供商(自己选)我的token是MaxminMax,然后一路执行:

image.pngimage.png

image.png

image.png

image.png

2.4.4、先把龙虾工具跑通再去关联聊天工具,所以选择Skip for now

image.png

2.4.5、搜索商先跳过

image.png

2.4.6、选择yes:

image.png

2.4.7、按下空格,选择Skip for now,然后enter

image.png

2.4.8、Google Places API Key ? 用不到,直接NO执行完事:

image.pngimage.pngimage.png

2.4.9、配置 Hooks

官方说明里,Hooks 用来“在某些命令触发时自动执行动作”(例如 /new 时做会话记忆整理)

  • 优先 session-memory,若列表里有Skip for now image.png

恭喜成功了

image.pngimage.png

2.4.11、假如失败(希望你的不会):

image.png 如果上述启动,提示Gateway网关失败, 可能原因

  • 端口18789被占用
  • 权限不足
  • 配置文件错误 记得,确认一下网关是否有安装启动 openclaw gateway install openclaw gateway start 解决方法
# Linux查看端口占用
lsof -i :18789
# Windows查看端口占用
netstat -ano | findstr :18789
# 或者换端口启动
openclaw gateway start --port 18790

3、openclaw常用命令

image.png

4、OpenClaw测试检查

1、检查openclaw服务是否一切启动正常 image.png

5、访问控制界面

# 启动 Web 控制台 openclaw dashboard 或在浏览器中访问:http://127.0.0.1:18789/

image.png

结束

  • 安装完成需要去PC管理界面跟龙虾对话,设置他的名称、角色、注意事项等,想到什么就叫他做什么,并记录下来,下次就能直接用。
  • 其实安装龙虾不难,难的是怎么使用,怎么配置让它动起手来干。今天第一步安装完成了,接下来就是训练龙虾,让龙虾在做事的过程进行记忆存储或者叫驯化进程了。

另外说下,我用龙虾帮忙开发了一个小程序,大家可以去看下,小程序名称:“心问有答”

gh_a14786fed1c9_258.jpg

❌