🔥【Agent】应用全流程

前置知识

相关资料

tokenizer 和 embedding,与传统类似

token:文本切分后的“最小单位”,

  • 中文:平均一个汉字一个 token,有多个汉字合并为一个 token 的情况,也有1个生僻字拆分成多个 token 的情况
  • 英文:1个token=3~4个字符,因此一个单词平均 1~2个 token(长词会更多)

一个展示token的网站:http://tiktokenizer.vercel.app/

embedding:每个token对应的向量,可能是 768、2048、4096 等,取决于模型,也是模型训练时,需要更新的参数。

一个 GPT 模型可视化的网站:https://bbycroft.net/llm

Prompt

好的问题才有好的答案(另一篇博客)

实际上,RAG、Zero Shot、One Shot、Few Shot 都通过操作 Prompt 满足目的。

OpenAI

模型的调用方式很多,例如用 request 调用。大多数可以用 OpenAI 方式调用

# pip install openai
from openai import OpenAI

api_key = "xxx" # 如有密钥管理,用 api_key = os.getenv("DEEPSEEK_API_KEY)
base_url="https://xxx.xxx.com/v1/" # 注意 base_url 不要加 chat/completions

client = OpenAI(api_key=api_key, base_url=base_url)

# %% 简单调用
resp = client.chat.completions.create(
    model="DeepSeek-V3.2",
    messages=[
        {"role": "system", "content": "你是一个简洁的助手。"},
        {"role": "user", "content": "用一句话解释什么是YOLO标注格式。"},
    ],
)

print(resp.choices[0].message.content)

# %% 流式输出
stream = client.chat.completions.create(
    model="DeepSeek-V3.2",
    messages=[{"role": "user", "content": "写一个Python函数,判断素数。"}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta
    if delta and delta.content:
        print(delta.content, end="", flush=True)
print()

# %% 多模态
import base64


def b64_image(path):
    return base64.b64encode(open(path, "rb").read()).decode()


resp = client.chat.completions.create(
    model="Qwen3-VL-32B-Instruct",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "这张图里有什么?"},
            {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_image('1.png')}"}},
            # {"type": "image_url", "image_url": {"url": "https://xxx/1.png"}},
        ],
    }],
)

print(resp.choices[0].message.content)

#%% embedding

resp = client.embeddings.create(
    model="Qwen3-Embedding-0.6B",
    input=[
        "外卖不好吃",
        "送的很快",
        "太油了,太辣了"
    ],
)

# 每一条对应 resp.data 的一条
vec0 = resp.data[0].embedding  # list<float>

关于密钥管理

# 方式1:临时设置(仅限于当前终端会话)
export DEEPSEEK_API_KEY="xxx"

# 方式2:永久设置
echo 'export DEEPSEEK_API_KEY="your-api-key-here"' >> ~/.zshrc
source ~/.zshrc

# 方式3:项目级别,创建 .env 文件
DEEPSEEK_API_KEY=your-api-key-here

# 对应的 Python:
api_key = os.getenv("DEEPSEEK_API_KEY")

技能

MCP

LLM 调用工具,有一段演化历史

  • 第一代,由 OpenAI 调用各种工具。具体做法是:工具开发者上传一个 yml 文件,yml写明 description、调用方法等,用户使用的时候最多手动选择3个 tool
  • 第二代,由应用调用 LLM,然后 LLM 决定调用哪个函数,应用去调用这个函数
  • 第三代,模版生成 prompt,对 LLM 做 SFT,使其有调用能力

MCP(Model Context Protocol) 协议是 Claude 母公司 Anthropic 于 2024年11月开源发布,它提供了一种标准化方式连接不同的 LLM、工具

一些 MCP 市场

  • mcp.so
  • mcpmarket.com
  • smithery.ai

MCP 原理

sequenceDiagram autonumber participant 用户 participant Cline participant MCP Server participant LLM rect rgb(240, 248, 255) Note over Cline,MCP Server: 启动(只运行一次) Cline ->> MCP Server: 启动 MCP Server Cline ->> MCP Server: 你好,我是 Cline MCP Server -->> Cline: 你好,我是 get_info Cline ->> MCP Server: 你有哪些工具? MCP Server -->> Cline: 我有 get_my_info,... end loop 每次用户提问(可反复运行) 用户 ->> Cline: Guo Fei 的 GitHub 地址是? Cline ->> LLM: Guo Fei 的 GitHub 地址是?我还有一些工具... LLM -->> Cline: 我要调用 get_my_info,参数是... Cline ->> MCP Server: 我要调用 get_my_info,参数是... MCP Server -->> Cline: 调用完毕,结果是xxx Cline ->> LLM: 调用完毕,结果是xxx LLM -->> Cline: Guofei 的 Github 地址是xxx Cline -->> 用户: Guofei 的 Github 地址是xxx end

  1. Cline 可以换成别的,例如 Codex
  2. Cline 和 MCP Server 之间的对话都是 XML/Json 格式的,上面的图为了好理解才写成自然语言
  3. MCP 协议本身并没有规定如何与 LLM 交互,与 LLM 交互是由 Cline 决定的。Cline 调用 LLM 的 Prompt 极其复杂(几万字),包含 tool 用法、各种指令( XML、Function Calling 之类的)
    • Cline 与 LLM 的交互可能为多轮,直到得到最终结果
    • 这个多轮交互,可以抽象为 思考-行动-观察(Think-Action-Observation)的循环,并最终用一个结束标志(本身是一个 Action)来结束整个流程。这个思想出自 ReAct(2022年的一篇论文)
    • 每次交互,本身也是 stream 模型,LLM 会连续吐出 Token

MCP 实操

step1:环境配置

# 创建 MCP 项目
mkdir my_mcp
cd my_mcp

# 建立虚拟环境
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install "mcp[cli]"

# 不要手动运行,仅作为测试使用
# python server.py

step2:写代码 保存为 server.py(别的名字也可以,之后的配置按照实际名字更改)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("get_info", log_level="ERROR")


@mcp.tool()
async def get_my_info(channel: str) -> str:
    # 下面的函数注释会先被 LLM 拿到
    """
    Get Guo Fei's public information by channel.

    Available channels (must match exactly):
    - "个人网站": personal website
    - "GitHub": GitHub profile
    - "知乎": Zhihu profile

    Args:
        channel: One of the predefined channel names above.

    Returns:
        A URL string corresponding to the requested channel.

    If an invalid channel is provided, a readable error message is returned.
    """

    info = {
        "个人网站": "https://www.guofei.site/"
        , "GitHub": "https://github.com/guofei9987/"
        , "知乎": "https://www.zhihu.com/people/guofei9987/answers/by_votes"
    }

    return info.get(channel, f"无效的渠道名称:{channel}。请使用以下之一:{', '.join(info.keys())}")


if __name__ == "__main__":
    mcp.run(transport='stdio')

step3:配置工具:(用 CLINE)。

  • 在 VSCode 中,安装 CLINE
  • 在 CLINE 中,配置 MCP
  • 配置文件如下:(注意,地址按照实际地址更改,最好是绝对路径)
    "get_my_info": {
        "autoApprove": [],
        "disabled": false,
        "timeout": 60,
        "command": "~/my_mcp/.venv/bin/python",
        "args": [
          "~/my_mcp/server.py"
        ],
        "type": "stdio"
      }
    

step4:使用 agent

  • 输入测试语句:调用 get-my-info 工具,channel = github
  • agent 返回的输出:成功调用 get-my-info 工具,channel = github,返回结果为:[](https://github.com/guofei9987/)<https://github.com/guofei9987/>

step3-1:别的工具也可以

  • Codex:配置文件是这样的:
    [mcp_servers.get_my_info]
    command = "~/my_mcp/.venv/bin/python"
    args = ["~/my_mcp/server.py"]
    

step4_1:使用

  • 输入:Guo Fei 的 GitHub 地址是?
  • 输出:Guo Fei 的 GitHub 地址是 https://github.com/guofei9987/

SKILL

结构是这样的:

  • 由很多 xxx.md 组成的
  • 这些 xxx.md 文件都在头部有自己的 meta 信息,这些 meta 信息可以给到大模型
  • xxx.md 还可以有链接,指向别的 md 文件 (“子技能”),这个机制叫做 渐进式披露
  • 还有脚本 xxx.py,在提示词中写清楚其用法、调用方式

RAG

为什么需要 RAG? Prompt 解决不了的问题:

  • 缺乏知识:涉及最新的知识(例如某个新上映的电影),或者特定专业知识(例如内网的某个办公应用如何配置),大模型没有学习过,所以表现不好
  • 幻觉问题:大模型不了解自己的知识边界,对于欠缺知识的领域仍然尝试回答,错误的回答内容具有迷惑性

解决方案:RAG(Retrieval-Augmented Generation,检索增强生成)

  • 本质上就是把搜索到的资料(例如企业信息、知识库等)作为提示词的一部分发给 LLM,让 LLM 有根据地输出内容,从而提高回答的准确性、相关性、新鲜度,并解决幻觉问题。
    • OpenAI:RAG 可以将回答准确率从 45% 提升到了 98%
  • 相当于给大模型装上了“知识外挂”,基础大模型不用重新训练即可随时调用特定领域知识。省下了重新训练的成本。
  • 技术实现上,涉及数据检索、信息增强、AI 生成等多个过程

RAG步骤

  • 准备阶段
    • 文档结构化,把 json、pdf、html、图片等等,转化为 txt
    • 分块(Chunking),把文档切分为小块
      • 常见方法
        • 固定分块:每个分块512个,要有重叠(overlapping)
        • 固定规则:例如用标点、段落分块
        • 语义分块:使用文本类模型做区块识别
      • 分块原则
        • 尽可能保留关键信息
        • 效果评估:后面
    • Embedding,把小块映射为向量,可以提高检索性能
    • 构建索引(Indexing) ,把这些向量存到向量数据库/索引。一些优化策略
      • 根据扩展信息,如标签、摘要、关联问题
      • 链式索引、树索引、图索引
  • 调用阶段
    • Query Rewrite:把用户问题改写为合适检索的问题
      • 例如,用户问“iPhone15 Pro 和 iPhone16 哪个续航长”,就需要拆分成两个问题去检索
      • 又例如,错别字、不通顺的表达、指代不明、玩梗等
      • 可选:语义校验模块,用来判断改写的质量
    • 检索(Retrieval):各种检索
    • 重排(Rerank):目的是提升检索精度、精简后续对模型的输入
      • 常见方法:用模型重排、结合业务策略(时间、用户属性、标签等)
    • Prompt工程(核心技术),如何把检索结果很好的放入 Prompt
    • 大模型根据 Prompt 做出回答

加入到 Prompt 是这样的:

"""
基于提供的【相关材料】,回答最后的问题
## 【相关材料】
{context}

## 最后的问题
问题:{input}
"""

RAG 的效果评估,评估工具:RAGAS

  • 检索的评估
    • 召回率:是否检索出了 Top N 内容
    • 相关性:召回的内容与 query 的相关性
    • 业务维度:例如地域、时效等
  • 生成的评估
    • faithful 忠实性:答案是否是从召回的内容中推断出来的
    • answer relevance 答案相关性:回答和召回内容,与问题的相关性
    • answer similarity 答案相似性:回答和标准答案的相似性
    • 事实准确性:回答是否匹配用户问题
  • 业务指标:用户满意率、首轮成功率

RAG优化

  1. Chunking 阶段:用模型基于语义做切分
  2. rewrite

RAG 实操

准备工作与说明:

  • 大模型(OpenAI 可调用)
  • 相关的包安装
    pip install -U transformers sentence-transformers
    pip install -U langchain langchain-community langchain-huggingface huggingface_hub
    # 向量数据库:
    pip install -U chromadb
    pip install -U openai
    
  • 模型下载(为了展示,选用一些轻量级的模型)
    • 召回:https://huggingface.co/BAAI/bge-small-zh-v1.5
    • 重排:https://huggingface.co/cross-encoder/mmarco-mMiniLMv2-L12-H384-v1/
  • 原始文档,这里用 prices.txt 做示例:
    import pooch
    pooch.retrieve(
      url='http://guofei.site/datasets/nlp/prices.txt',
      fname='prices.txt', path='.')
    

说明:各个环节都比较粗暴,实际应用可以按照上面的理论篇对每个环节优化

  • Chunking,简单用换行符分割
  • 召回模型、重排模型,都用的很小的模型
  • 没有做 Query Rewrite

先不用 RAG 试一下

query = "iPhone 15 Pro Max 多少钱"


payload = {
    'model': 'DeepSeek-V3.2',
    'messages': [
        {
            'role': 'system',
            'content': '你是一个助手'
        },
        {
            'role': 'user',
            'content': query
        }
    ],
}

res = call_model(payload)

print("未加入 RAG的结果:")
print(res)

结果就不贴了,又长又假

使用 RAG,step1:chunking 和 向量数据库构建

# %% Chunking
def split_into_chunks(doc_file: str) -> List[str]:
    with open(doc_file, 'r') as f:
        content = f.read()
    return content.split('\n')


chunks = split_into_chunks("prices.txt")

# %% Embedding

from sentence_transformers import SentenceTransformer

embed_model = SentenceTransformer("./bge-small-zh-v1.5")


def embed_chunk(chunk: str) -> List[float]:
    embedding = embed_model.encode(chunk)
    return embedding.tolist()


test_embedding = embed_chunk("测试")
print("embedding 的维度", len(test_embedding))

embeddings = [embed_chunk(chunk) for chunk in chunks]

# meta 也可以参与召回
metas = [{"source": "prices.txt"} for chunk in chunks]

print("embedding 数量(片段个数)", len(embeddings))

# %% embedding 存入向量数据库中
import chromadb
from typing import List, Dict


# chromadb_client = chromadb.EphemeralClient() # 数据存在内存中
chromadb_client = chromadb.PersistentClient(path="./chroma_db")

chromadb_collection = chromadb_client.get_or_create_collection(name="default")


def save_embedding(chunks: List[str], embeddings: List[List[float]], metas: List[Dict]) -> None:
    ids = [str(i) for i in range(len(embeddings))]
    chromadb_collection.add(
        documents=chunks,
        embeddings=embeddings,
        ids=ids
        , metadatas=metas
    )


save_embedding(chunks, embeddings, metas)

step2:召回、重排,并输出结果

from typing import List, Dict

from sentence_transformers import SentenceTransformer

embed_model = SentenceTransformer("./bge-small-zh-v1.5")


def embed_chunk(chunk: str) -> List[float]:
    embedding = embed_model.encode(chunk)
    return embedding.tolist()



#%% 加载向量数据库


import chromadb

chromadb_client = chromadb.PersistentClient(path="./chroma_db")

chromadb_collection = chromadb_client.get_or_create_collection(name="default")
print(f"数据库已加载 {chromadb_collection.count()} 条数据")

# %% 召回

def retrieve(query: str, top_k: int) -> List[str]:
    query_embedding = embed_chunk(query)
    results = chromadb_collection.query(
        query_embeddings=query_embedding,
        n_results=top_k
    )
    return results['documents'][0]


query = "iPhone 15 Pro Max 多少钱"

retrieved_chunks = retrieve(query, 10)

print("召回:", retrieved_chunks)

# %% 重排
from sentence_transformers import CrossEncoder

cross_encoder = CrossEncoder("./mmarco-mMiniLMv2-L12-H384-v1")


def rerank(query: str, retrieved_chunks: List[str], top_k: int = 3) -> List[str]:
    pairs = [(query, chunk) for chunk in retrieved_chunks]
    scores = cross_encoder.predict(pairs)

    chunk_with_scores = [(chunk, score)
                         for chunk, score in zip(retrieved_chunks, scores)]

    chunk_with_scores.sort(key=lambda x: x[1], reverse=True)

    return [chunk for chunk, _ in chunk_with_scores][:top_k]


reranked_chunks = rerank(query, retrieved_chunks)

print("重排后:", reranked_chunks)

# %% 测试结果:


query = "保温杯多少钱"
query = "皮鞋多少钱"
query = "How much is a milk"

retrieved_chunks = retrieve(query, top_k=10)
reranked_chunks = rerank(query, retrieved_chunks, top_k=3)

print("query:", query)
print("retrieved_chunks:", retrieved_chunks)
print("reranked_chunks:", reranked_chunks)


from openai import OpenAI

client = OpenAI(
    api_key="xxx",
    base_url="https://xxx.xxx.com/v1/"
)

resp = client.chat.completions.create(
    model="DeepSeek-V3.2",
    messages=[
        {
            'role': 'system',
            'content': f'''你是一个助手,请给予【相关材料】回答问题
# 【相关材料】
{"\n".join(reranked_chunks)}
            '''
        },
        {
            'role': 'user',
            'content': query
        }
    ],
)

print(resp.choices[0].message.content)

RAG 工程选型

先完整、再完善,以上流程整体上线后,各个部分都有优化的空间。收集用户反馈,用以下方法调整。

文档预处理

  • PDF:PyMuDPF、docling
  • python-docx
  • OCR:pytesseract
  • 图像 embedding:CLIP(可以同时理解图片内容和图片中的文本)
    • 除了图像本身做 embedding 之外,还可以用 VL 生成图像的描述,把这段描述也做 embedding 放入数据库中(向量数据库中有2条记录)
  • 另一个思路(更现代,效果更好):使用多模 embedding,它可以把图片、文本、视频等映射到同一个向量空间中,可以实现以文搜图、以图搜视频等
    • Qwen-VL-embedding
    • multimodal-embedding-v1(OpenAI提供的闭源模型)
  • 对于表格,比起 markdown,LLM 对 HTML 理解更好(2025 年的结论,以后可能会变化)

知识生成、知识完善:除了原 chunk 外:

  • 用 LLM 生成一些知识,放入向量数据库。
  • 用 LLM 生成一些问题&回答,放入向量数据库

知识健康度检查:收集用户的 query(这个 query 也可以用 LLM 模拟用户生成),建立一个 LLM 来检查知识库的健康度

  • 完整性:评估知识库是否覆盖用户的主要查询需求
  • 实效性:识别过期/需要更新的知识
  • 一致性:发现知识库中的冲突和矛盾信息
  • 综合评分:提供量化的健康度评分和改进建议
  • 工程上:对知识库做版本管理

以下是 LLM 做知识健康检查,只包含关键 prompt

完整性

检查标准:
1. 查询是否能在知识库中找到相关答案
2. 知识是否完整、准确
3. 是否覆盖了用户的主要需求
4. 是否存在知识空白

请返回JSON格式:
{
"missing_knowledge": [
    {
        "query": "测试查询",
        "missing_aspect": "缺少的知识方面",
        "importance": "重要性(高/中/低)",
        "suggested_content": "建议的知识内容",
        "category": "知识分类"
    }
],
"coverage_score": "覆盖率评分(0-1)",
"completeness_analysis": "完整性分析"
}

实效性

检查标准:
1. 时间相关信息是否过期(年份、日期、时间范围)
2. 价格信息是否最新(价格、费用、票价等)
3. 政策规则是否更新(政策、规定、规则等)
4. 活动信息是否有效(活动、节日、特殊安排等)
5. 联系方式是否准确(电话、地址、网址等)
6. 技术信息是否过时(版本、技术标准等)
请返回JSON格式:
{
"outdated_knowledge": [
{
"chunk_id": "知识切片ID",
"content": "知识内容",
"outdated_aspect": "过期方面",
"severity": "严重程度(高/中/低)",
"suggested_update": "建议更新内容",
"last_verified": "最后验证时间"
}
],
"freshness_score": "新鲜度评分(0-1)",
"update_recommendations": "更新建议"
} 


当前时间是 {datetime.now().strftime('%Y年%m月%d日')}

一致性

检查标准:
1. 同一主题的不同说法(地点、名称、描述等)
2. 价格信息的差异(价格、费用、收费标准等)
3. 时间信息的不一致(营业时间、开放时间、活动时间等)
4. 规则政策的冲突(规定、政策、要求等)
5. 操作流程的差异(步骤、方法、流程等)
6. 联系方式的差异(地址、电话、网址等)
请返回JSON格式:
{
"conflicting_knowledge": [
{
"conflict_type": "冲突类型",
"chunk_ids": ["相关切片ID"],
"conflicting_content": ["冲突内容"],
"severity": "严重程度(高/中/低)",
"resolution_suggestion": "解决建议"
}
],
"consistency_score": "一致性评分(0-1)",
"conflict_analysis": "冲突分析"
}

切片策略 (主要就是前2个)

  • 固定长度
  • LLM 做切片:质量高、成本高
  • 句子边界
  • 按标题/章节切分:markdown 这种高度结构化的文档
  • 滑动窗口

embedding

  • 参考 https://www.guofei.site/2018/09/24/nlp_feature.html 关于 embedding 的部分
  • 模型上,前面的例子用的是 sentence_transformers 加载一个模型
  • 也可以调用 Qwen3-embedding 模型(用 openai 调用),这些更现代的模型会更准
  • 可以从 huggingface 上找,国内:https://modelscope.cn/

常见模型

  • 常见的通用 embedding 模型
    • BGE-M3 M3(智源研究院):支持100+语言,输入长度达8192 tokens,融合密集、稀疏、多向量混合检索,适合跨语言长文档检索。
    • text-embedding-3-large(OpenAI):向量维度3072,长文本语义捕捉能力强,英文表现优秀。
    • Jina-embeddings-v2-small(Jina AI)。特点:参数量仅35M,支持实时推理(RT<50ms),适合轻量化部署。
  • 常见的中文 embedding 模型
    • xiaobu-embedding-v2:针对中文语义优化,语义理解能力强。
    • M3E-Base:针对中文优化的轻量模型,适合本地私有化部署。适用场景:中文法律、医疗领域检索任务。文件大小:0.4G (m3e-base)
    • stella-mrl-large-zh-v3.5-1792:处理大规模中文数据能力强,捕捉细微语义关系。适用场景:中文文本高级语义分析、自然语言处理任务。

多模态召回优化:用户问题是文本,它与 视频、图片距离天然就远,但是需求是能返回图片和视频

  • 做法:识别召回视频、图片的场景,并指定召回 top-k 个 图片/视频(用 meta 来指定)

关键词检索:from rank_bm25 import BM250api

  • 这是一个基于关键词(TF-IDF)的召回算法库
  • 作为补充召回
  • 用参数 alpha 控制: 关键词检索 vs 向量检索的占比
    • alpha 越小,越偏重关键词检索。关键词检索偏向于专业术语、精确匹配的需求。向量检索偏向于同义词丰富、表述多样的场景。

代码如下:

保存bm25模型

def save_bm25_model(chunks: List[str], model_path: str) -> None:
    tokenized_chunks = [tokenize_chinese(chunk) for chunk in chunks]
    bm25 = BM25Okapi(tokenized_chunks)

    with open(model_path, "wb") as f:
        pickle.dump({"bm25": bm25, "chunks": chunks}, f)

save_bm25_model(chunks, model_path='bm25.pkl')

加载 bm25模型,并召回

def load_bm25_model(model_path: str) -> Tuple[List[str], BM25Okapi]:
    with open(model_path, 'rb') as f:
        data = pickle.load(f)

    bm25: BM25Okapi = data["bm25"]
    chunks: List[str] = data["chunks"]
    return chunks, bm25


# %%


def retrieve_bm25(query, bm25, chunks, k=5):
    tokenized_query = tokenize_chinese(query)
    bm25_scores = bm25.get_scores(tokenized_query)

    ranked_idx = sorted(range(len(chunks)), key=lambda i: bm25_scores[i], reverse=True)[:k]
    res = [(chunks[i], float(bm25_scores[i])) for i in ranked_idx]
    return res


chunks, bm25 = load_bm25_model("bm25.pkl")

k = 5
query = "iPhone 15 Pro Max 多少钱"
query = "羽绒服多少钱"

res = retrieve_bm25(query, bm25, chunks)

向量数据库

  • 前面的例子用的是 Chroma,它打包了“向量+文档+元数据+持久化+部署模式”,适合快速验证
  • 线上建议使用:
    • FAISS:性能标杆
    • Elasticsearch:混合搜索(关键词+向量)表现优异
    • Milvus、Pinecon、Weaviate、Qdrant 等

:caption: RAG 的几种架构


query 改写和联网搜索

query 改写

# 参考前面,传入 token
# client = OpenAI(...)


# 基于 prompt 生成文本
def get_completion(prompt, model="DeepSeek-V3.2"):
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}]
    )

    return resp.choices[0].message.content


# Query改写功能
class QueryRewriter:
    def __init__(self, model="DeepSeek-V3.2"):
        self.model = model

    def rewrite_context_dependent_query(self, current_query, conversation_history):
        """上下文依赖型Query改写"""
        instruction = """
你是一个智能的查询优化助手。请分析用户的当前问题以及前序对话历史,判断当前问题是否依赖于上下文。
如果依赖,请将当前问题改写成一个独立的、包含所有必要上下文信息的完整问题。
如果不依赖,直接返回原问题。
"""

        prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 当前问题 ###
{current_query}

### 改写后的问题 ###
"""

        return get_completion(prompt, self.model)

    def rewrite_comparative_query(self, query, conversation_history):
        """对比型Query改写"""
        instruction = """
你是一个查询分析专家。请分析用户的输入和相关的对话上下文,识别出问题中需要进行比较的多个对象。
然后,将原始问题改写成一个更明确、更适合在知识库中检索的对比性查询。
"""

        prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 原始问题 ###
{query}

### 改写后的查询 ###
"""

        return get_completion(prompt, self.model)

    def rewrite_ambiguous_reference_query(self, current_query, conversation_history):
        """模糊指代型Query改写"""
        instruction = """
你是一个消除语言歧义的专家。请分析用户的当前问题和对话历史,找出问题中 "都"、"它"、"这个" 等模糊指代词具体指向的对象。
然后,将这些指代词替换为明确的对象名称,生成一个清晰、无歧义的新问题。
"""

        prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 当前问题 ###
{current_query}

### 改写后的问题 ###
"""

        return get_completion(prompt, self.model)

    def rewrite_multi_intent_query(self, current_query, conversation_history=None):
        """多意图型Query改写 - 分解查询
        ❗️ 多意图应当返回 List<str>,这个与别的不一样"""
        instruction = """
你是一个任务分解机器人。请将用户的复杂问题分解成多个独立的、可以单独回答的简单问题。以JSON数组格式输出。
"""

        prompt = f"""
### 指令 ###
{instruction}

### 原始问题 ###
{current_query}

### 分解后的问题列表 ###
请以JSON数组格式输出,例如:["问题1", "问题2", "问题3"]
"""

        return get_completion(prompt, self.model)

    def rewrite_rhetorical_query(self, current_query, conversation_history):
        """反问型Query改写"""
        instruction = """
你是一个沟通理解大师。请分析用户的反问或带有情绪的陈述,识别其背后真实的意图和问题。
然后,将这个反问改写成一个中立、客观、可以直接用于知识库检索的问题。
"""

        prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 当前问题 ###
{current_query}

### 改写后的问题 ###
"""

        return get_completion(prompt, self.model)

    def rewrite(self, query, conversation_history, query_type: str):
        '''路由:根据 query_type 决定 调用'''
        if '上下文依赖' in query_type:
            final_result = self.rewrite_context_dependent_query(query, conversation_history)
        elif '对比' in query_type:
            final_result = self.rewrite_comparative_query(query, conversation_history)
        elif '模糊指代' in query_type:
            final_result = self.rewrite_ambiguous_reference_query(query, conversation_history)
        elif '多意图' in query_type:
            final_result = self.rewrite_multi_intent_query(query)
        elif '反问' in query_type:
            final_result = self.rewrite_rhetorical_query(query, conversation_history)
        else:
            final_result = query

        return final_result

    def query_type_classifier(self, query, conversation_history="", context_info=""):
        """自动识别Query类型并进行改写"""
        instruction = """
你是一个智能的查询分析专家。请分析用户的查询,识别其属于以下哪种类型:
1. 上下文依赖型 - 包含"还有"、"其他"等需要上下文理解的词汇
2. 对比型 - 包含"哪个"、"比较"、"更"、"哪个更好"、"哪个更"等比较词汇
3. 模糊指代型 - 包含"它"、"他们"、"都"、"这个"等指代词
4. 多意图型 - 包含多个独立问题,用"、"或"?"分隔
5. 反问型 - 包含"不会"、"难道"等反问语气
说明:如果同时存在多意图型、模糊指代型,优先级为多意图型>模糊指代型

请返回JSON格式的结果:
{
    "query_type": "查询类型",
    "confidence": "置信度(0-1)"
}
"""

        prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 上下文信息 ###
{context_info}

### 原始查询 ###
{query}

### 分析结果 ###
"""

        response = get_completion(prompt, self.model)
        try:
            return json.loads(response)
        except:
            return {
                "query_type": "未知类型",
                "confidence": 0.5
            }

    def classify_and_rewrite(self, query, conversation_history="", context_info=""):
        query_type_predict = self.query_type_classifier(query, conversation_history, context_info)
        query_type = query_type_predict["query_type"]
        res = {"query_type": query_type_predict["query_type"]
            , "confidence": query_type_predict["confidence"]
            , "rewrite_result": self.rewrite(query, conversation_history, query_type)
               }
        return res


def main():
    # 初始化Query改写器
    rewriter = QueryRewriter()
    print("=== Query改写功能使用示例(迪士尼主题乐园) ===\n")
    cases = [
        {"sample_name": "示例1: 上下文依赖型Query"
            , "conversation_history": """
用户: "我想了解一下上海迪士尼乐园的最新项目。"
AI: "上海迪士尼乐园最新推出了'疯狂动物城'主题园区,这里有朱迪警官和尼克狐的互动体验。"
用户: "这个园区有什么游乐设施?"
AI: "'疯狂动物城'园区目前有疯狂动物城警察局、朱迪警官训练营和尼克狐的冰淇淋店等设施。"
"""
            , "current_query": "还有其他设施吗"
            , "query_type": "上下文依赖"
         }

        , {"sample_name": "示例2: 对比型Query"
            , "conversation_history": """
用户: "我想了解一下上海迪士尼乐园的最新项目。"
AI: "上海迪士尼乐园最新推出了疯狂动物城主题园区,还有蜘蛛侠主题园区"
"""
            , "current_query": "哪个游玩的时间比较长,比较有趣"
            , "query_type": "对比"

           }

        , {"sample_name": "示例3: 模糊指代型Query"
            , "conversation_history": """
用户: "我想了解一下上海迪士尼乐园和香港迪士尼乐园的烟花表演。"
AI: "好的,上海迪士尼乐园和香港迪士尼乐园都有精彩的烟花表演。"
"""
            , "current_query": "都什么时候开始?"
            , "query_type": "模糊指代"

           }

        , {"sample_name": "示例4: 多意图型Query"
            , "conversation_history": None
            , "current_query": "门票多少钱?需要提前预约吗?停车费怎么收?"
            , "query_type": "多意图"

           }
        , {"sample_name": "示例5: 反问型Query"
            , "conversation_history": """
用户: "你好,我想预订下周六上海迪士尼乐园的门票。"
AI: "正在为您查询... 查询到下周六的门票已经售罄。"
用户: "售罄是什么意思?我朋友上周去还能买到当天的票。"
"""
            , "current_query": "难道还要提前一个月预订吧?"
            , "query_type": "反问"

           }

    ]

    for case in cases:
        print('\n' + "=" * 20)
        conversation_history = case['conversation_history']
        current_query = case['current_query']
        query_type = case['query_type']
        print(case['sample_name'])
        print(f"对话历史: {conversation_history}")
        print(f"当前查询: {current_query}")

        type_predict = rewriter.query_type_classifier(current_query, conversation_history, query_type)
        print(f"预测的对话类型{type_predict}")
        result = rewriter.rewrite(current_query, conversation_history, query_type)
        print(f"改写结果: {result}\n")

        result = rewriter.classify_and_rewrite(current_query, conversation_history)
        print(f"自动分类+改写结果: {result}")


if __name__ == "__main__":
    main()

联网检索


# 参考前面,传入 token
# client = OpenAI(...)


# 基于 prompt 生成文本
def get_completion(prompt, model="DeepSeek-V3.2"):
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}]
    )

    return resp.choices[0].message.content


# Query联网搜索改写功能
# 导入依赖库
import os
import json
import re
from datetime import datetime


class WebSearchQueryRewriter:
    def __init__(self, model="DeepSeek-V3.2"):
        self.model = model

    def identify_web_search_needs(self, query, conversation_history=""):
        """识别查询是否需要联网搜索"""
        instruction = """
你是一个智能的查询分析专家。请分析用户的查询,判断是否需要联网搜索来获取最新、最准确的信息。

需要联网搜索的情况包括:
1. 时效性信息 - 包含"最新"、"今天"、"现在"、"实时"、"当前"等时间相关词汇
2. 价格信息 - 包含"多少钱"、"价格"、"费用"、"票价"等价格相关词汇
3. 营业信息 - 包含"营业时间"、"开放时间"、"闭园时间"、"是否开放"等营业状态
4. 活动信息 - 包含"活动"、"表演"、"演出"、"节日"、"庆典"等动态信息
5. 天气信息 - 包含"天气"、"下雨"、"温度"等天气相关
6. 交通信息 - 包含"怎么去"、"交通"、"地铁"、"公交"等交通方式
7. 预订信息 - 包含"预订"、"预约"、"购票"、"订票"等预订相关
8. 实时状态 - 包含"排队"、"拥挤"、"人流量"等实时状态

请返回JSON格式:
{
    "need_web_search": true/false,
    "search_reason": "需要搜索的原因",
    "confidence": "置信度(0-1)"
}
"""

        prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 用户查询 ###
{query}

### 分析结果 ###
"""

        response = get_completion(prompt, self.model)
        try:
            return json.loads(response)
        except:
            return {
                "need_web_search": False,
                "search_reason": "无法解析",
                "confidence": 0.5
            }

    def rewrite_for_web_search(self, query, search_type="general"):
        """为联网搜索改写查询"""
        instruction = """
你是一个专业的搜索查询优化专家。请将用户的查询改写为更适合搜索引擎检索的形式。

改写技巧:
1. 添加具体地点 - 如"上海迪士尼乐园"、"香港迪士尼乐园"
2. 添加时间范围 - 如"2024年"、"今天"、"本周"
3. 使用关键词组合 - 将长句拆分为关键词
4. 添加搜索意图 - 明确搜索目的
5. 去除口语化表达 - 转换为标准搜索词
6. 添加相关词汇 - 增加同义词或相关词

请返回JSON格式:
{
    "rewritten_query": "改写后的搜索查询",
    "search_keywords": ["关键词1", "关键词2", "关键词3"],
    "search_intent": "搜索意图",
    "suggested_sources": ["建议搜索的网站类型"]
}
"""

        prompt = f"""
### 指令 ###
{instruction}

### 原始查询 ###
{query}

### 搜索类型 ###
{search_type}

### 改写结果 ###
"""

        response = get_completion(prompt, self.model)
        try:
            return json.loads(response)
        except:
            return {
                "rewritten_query": query,
                "search_keywords": [query],
                "search_intent": "信息查询",
                "suggested_sources": ["官方网站", "旅游网站"]
            }

    def generate_search_strategy(self, query, search_type="general"):
        """生成搜索策略"""
        current_date = datetime.now().strftime("%Y年%m月%d日")
        instruction = f"""
你是一个搜索策略专家。请为用户的查询制定详细的搜索策略。

当前日期:{current_date}

搜索策略包括:
1. 主要搜索词 - 核心关键词
2. 扩展搜索词 - 相关词汇和同义词
3. 搜索网站 - 推荐的搜索平台
4. 时间范围 - 具体的搜索时间范围

请返回JSON格式:
primary_keywords
"""

        prompt = f"""
### 指令 ###
{instruction}

### 用户查询 ###
{query}

### 搜索类型 ###
{search_type}

### 搜索策略 ###
"""

        response = get_completion(prompt, self.model)
        try:
            return json.loads(response)
        except:
            return {
                "primary_keywords": [query],
                "extended_keywords": [],
                "search_platforms": ["百度", "谷歌"],
                "time_range": "最近一周"
            }

    def auto_web_search_rewrite(self, query, conversation_history=""):
        """自动识别并改写为联网搜索查询"""
        # 第一步:识别是否需要联网搜索
        search_analysis = self.identify_web_search_needs(query, conversation_history)

        if not search_analysis.get('need_web_search', False):
            return {
                "need_web_search": False,
                "reason": "查询不需要联网搜索",
                "original_query": query
            }

        # 第二步:改写查询
        rewritten_result = self.rewrite_for_web_search(query)

        # 第三步:生成搜索策略
        search_strategy = self.generate_search_strategy(query)

        return {
            "need_web_search": True,
            "search_reason": search_analysis.get('search_reason', ''),
            "confidence": search_analysis.get('confidence', 0.5),
            "original_query": query,
            "rewritten_query": rewritten_result.get('rewritten_query', query),
            "search_keywords": rewritten_result.get('search_keywords', []),
            "search_intent": rewritten_result.get('search_intent', ''),
            "suggested_sources": rewritten_result.get('suggested_sources', []),
            "search_strategy": search_strategy
        }


def main():
    # 初始化联网搜索Query改写器
    web_searcher = WebSearchQueryRewriter()

    print("=== Query联网搜索识别与改写示例(迪士尼主题乐园) ===\n")

    # 示例1: 时效性信息查询
    print("示例1: 时效性信息查询")
    conversation_history1 = """
用户: "我想去上海迪士尼乐园玩"
AI: "上海迪士尼乐园是一个很棒的选择!"
"""
    query1 = "上海迪士尼乐园今天开放吗?现在人多不多?"

    print(f"对话历史: {conversation_history1}")
    print(f"当前查询: {query1}")

    result1 = web_searcher.auto_web_search_rewrite(query1, conversation_history1)

    if result1['need_web_search']:
        print(f"✓ 需要联网搜索")
        print(f"  搜索原因: {result1['search_reason']}")
        print(f"  置信度: {result1['confidence']}")
        print(f"  改写查询: {result1['rewritten_query']}")
        print(f"  搜索关键词: {result1['search_keywords']}")
        print(f"  搜索意图: {result1['search_intent']}")
        print(f"  建议来源: {result1['suggested_sources']}")
        print(f"  搜索策略:")
        print(f"    - 主要关键词: {result1['search_strategy']['primary_keywords']}")
        print(f"    - 扩展关键词: {result1['search_strategy']['extended_keywords']}")
        print(f"    - 搜索平台: {result1['search_strategy']['search_platforms']}")
        print(f"    - 时间范围: {result1['search_strategy']['time_range']}")
    else:
        print(f"✗ 不需要联网搜索")
        print(f"  原因: {result1['reason']}")

    print("\n" + "=" * 60 + "\n")

    # 示例2: 价格和预订信息查询
    print("示例2: 价格和预订信息查询")
    query2 = "下周六的门票多少钱?需要提前多久预订?"

    print(f"当前查询: {query2}")

    result2 = web_searcher.auto_web_search_rewrite(query2)

    if result2['need_web_search']:
        print(f"✓ 需要联网搜索")
        print(f"  搜索原因: {result2['search_reason']}")
        print(f"  置信度: {result2['confidence']}")
        print(f"  改写查询: {result2['rewritten_query']}")
        print(f"  搜索关键词: {result2['search_keywords']}")
        print(f"  搜索意图: {result2['search_intent']}")
        print(f"  建议来源: {result2['suggested_sources']}")
        print(f"  搜索策略:")
        print(f"    - 主要关键词: {result2['search_strategy']['primary_keywords']}")
        print(f"    - 扩展关键词: {result2['search_strategy']['extended_keywords']}")
        print(f"    - 搜索平台: {result2['search_strategy']['search_platforms']}")
        print(f"    - 时间范围: {result2['search_strategy']['time_range']}")
    else:
        print(f"✗ 不需要联网搜索")
        print(f"  原因: {result2['reason']}")


if __name__ == "__main__":
    main()

检索策略

  • Query2Doc:把用户的查询改写成文档,
    • 用户查询:“如何提高深度学习模型的训练效率?”
    • 生成一段扩展文档: ```
      1. 使用更高效的优化算法,如AdamW或LAMB。
      2. 采用混合精度训练(Mixed Precision Training),减少显存占用并加速计算。
      3. 使用分布式训练技术,如数据并行或模型并行。
      4. 对数据进行预处理和增强,减少训练时的冗余计算。
      5. 调整学习率调度策略,避免训练过程中的震荡。 ```
  • doc2query:为文档生成相关的 query
  • Small-to-Big
    • 用户输入 query 后,系统先在小规模内容(摘要、关键句、段落)中检索。
    • 然后系统根据小规模内容,用指针(或文档ID、URL等),找到大规模内容
    • 把大规模内容作为 RAG 上下文,放入 prompt

GraphRAG 在工程中应用还不多,其思路可以借鉴。

传统 RAG 的缺点:

  • 连接点缺失。问题的答案分散在文档的不同位置,且没有直接的语义重叠,传统 RAG 很难把它们串联起来
  • 宏观理解缺失。如果用户问“这篇文章的主旨是什么?”传统RAG 很难给出一个概括性的答案,

GraphRAG 主要步骤(https://github.com/microsoft/graphrag)

  • 切片
  • 抽取:抽取的是 实体、关系。(知识图谱),用的是 LLM 方法,n个 prompt (很贵)
  • 聚类和摘要

Agent

Agent 是包含了 RAG、提示词、Function calling、Re-Act等一系列元素的集合

  • API 调用,例如查询天气、发送邮件
  • Re-Act

自主Agent

  • 由 Planner 模块(也是个LLM),与其它 LLM、工具调用的配合

Fine-tuning

Scaling Law LLaMA:即使是7B模型,训练数据继续扩展到 1T token,其性能仍然会上升

为什么需要微调?

  • 更改提示词方便快捷,但是有上限:
  • 领域模型经验难以描述
  • prompt 太长,遵循程度低。
  • prompt 遵循有幻觉
  • 哪些任务适合 SFT?
  • Prompt 放不下,例如某个系统有数万条规则。
  • 一些新的思考方式,例如日志分析大模型,可以让它学会沿着报错 stack 分析
  • 某些文风(例如角色扮演)、风险偏好对齐等等

有哪些思路

  • 大模型(如 DeepSeek-671B)在特定领域的知识转移到小模型如(Qwen-0.6B)。如此,可以在推理时获得双方的优点:小模型的QPS、小资源消耗,接近大模型准确率。
    • 方法和流程:大模型 -> 获得COT数据 -> 蒸馏小模型
  • 大模型->专家经验(Ground truth)->蒸馏小模型
    • 方法和流程:专家用人工的方式提供答案/推理(可以借助大模型,或者不借助大模型)。成本较高。
  • RSFT(拒绝采样 SFT):
    • 传统 SFT 数据是用大模型(如 DeepSeek-R1)生成 CoT 和答案。然后剔除低质量的,用于训练小模型。
    • RSFT 是让小模型A产生 CoT 和答案,用大模型判断质量并筛选,然后训练小模型A。
    • 因为训练数据来自 A 自己,它是会这些的,只是思路不能收敛于正确答案。这样可以帮助其思路收敛,因此效果很好。
  • RFT:RL+SFT,击溃是 RSFT 的基础上,用高质量的一组问题做回答和奖惩。

什么时候需要?

  • 如果是通识场景,不需要 SFT
  • 如果场景比较冷僻,就需要 SFT

有哪些方法

  • 全量微调(Full Finetune):所有参数都更新
  • 参数高效微调(PEFT,Parameter-Efficient-Finetune),仅调整部分参数
    • LoRA:通过低秩矩阵分解模拟权重更新
    • 还有 Adapter、Prefix Tuning 等
    • QLoRA:是量化 + LoRA,冻结的基座模型用 4bit 放显存
  • 强化学习(Reinforcement Learning)
    • 人类反馈强化学习(RLHF):人类对模型输出评价,使其符合人类价值观和任务需求
    • 近端策略优化(PPO):通过策略梯度更新
    • 直接偏好优化(DPO):无需显式训练奖励,是PPO的简化
    • 组相对策略优化(GRPO)

base model 选择

  • 复杂SFT任务:选 7B 以上
  • 高性能任务:选 1B 以下

获取数据

方法1直接用大模型生成COT。例如,直接用基座模型(例如 DeepSeek-R1/V3, ChatGPT)生成COT数据

方法2设计多智能体 例如,设计2个智能体。一个扮演客服专家,一个扮演客户

  • 客服专家产生话术
  • 客户提出意见,例如,“增加友好和关心的语气”、“这段话术稍显冗长”
  • 客服专家根据意见做优化
  • 数轮之后(3-5轮),得到优化后的话术。
  • 用另一个 LLM 做一轮过滤,得到最终的数据集

方法3:传统方法

  • 业务系统收集(预训练一般不用这个,因为有数据泄露风险,但 SFT 经常需要)
  • 开源数据集
    • 开源数据集
    • HuggingFace、GitHub
  • 爬虫。爬到的数据,解析阶段也可以用 LLM 了
  • 采买:题库、书籍、教材

高质量训练数据

数据不需要太多:Meta《LIMA》:1k优质样本即可打败海量普通数据。因此,获取 高质量训练数据 是关键

数据质量评估

  • 准确
    • 无事实错误
    • 一致性:内部处理逻辑统一,数据之间没有互相矛盾
    • 其它问题,拼写检查与纠正、文本规范化(如有必要的话,例如小写化、去除特殊字符)、确定适当的最大文本长度。
    • 方法和工具:
      • 人工质量抽检
      • rule-based:格式准确性等多重规则
      • LLM as judge
      • 构建静态指标:例如,输出长度/PPL等
        • PPL (困惑度,perplexity)的计算:预测下一个词时的“平均交叉熵”的指数。它大于1,并且越大代表越不确定。
        • PPL 过高和过低都要剔除。过低代表不必再学了,过高的也是学不会。
      • 借用开源 reward model/微调若干大模型来实现数据质量奖励/训练小模型
  • 完整性 (覆盖任务要点)
    • 覆盖全部需要的 任务类型
    • 难度梯度 简单/中等/复杂任务比例合理
    • 覆盖全部需要的 文本主题
    • 数据洁净度:剔除重复、噪声、错误的样本
    • 去除冗余数据
    • 正负样本均衡
    • 样本相关性,捕捉数据集的上下文和语义多样性。(生成的指令和其最相似的种子指令之间的ROUGE-L分数分布。)
    • kmeans/k-center(计算复杂度较低), 挑选出多样性的数据,即Seed Instruction Data。这步关注的是 coverage。
  • 简洁性:(无冗余信息)
    • 高质量的重复数据,可以提升模型效果。但中等质量会浪费计算资源、降低泛化性。

实操流程

  • 去重:md5MinHash
  • 低质量过滤
    • 关键词过滤、规则过滤
    • 用”小“模型(例如 Qwen-7B)计算 PPL(PPL太低、太高都不适合训练)
  • 去毒:黄、赌、毒、涉政、隐私去除
  • 数据集整体评估:消融实验。
    • 是什么:把新的数据集,在之前模型的 checkpoint 上继续训练,观察其在各个验证集(榜单)上的表现
    • 目的:评估新数据集是否:1)提供增量知识,2)不损害已训练的效果
    • 实验结果:效果提升,说明有提升;效果下降,说明损害了已训练的效果;效果不变,说明新数据集没有带来提升
    • 难点:多个验证集,有的提升,有的下降,怎么办?

业务知识如何融入

  • 专家的感性知识。典型历史案例 -> (大模型)抽取专家知识 -> 沉淀为业务知识
  • 策略/正则表达式/规则 -> 翻译成人话 -> 沉淀为专家经验
  • 业务专有名称 -> 整理出业务“词典”

SFT

如何训练

  • loss. SFT 时,一般会 mask 掉 prompt 部分的 loss,只学习输出部分的 loss
  • 学习率. 一般是预训练的学习率的 0.1 倍
  • epoch. 小样本(<1万)5个,大样本(>1万)2个
  • 混入 30%-50% 高质量预训练数据(通用数据),防止灾难性遗忘,保持泛化能力

关键指标

  • 业务合格率:从业务出发,人工对结果打标,给出 good/ok/bad,并计算指标
  • 提升率:指标比上一个版本提升了多少

LoRA(Low-Rank Adaptation of Large Language Models)

  • 核心原理是矩阵分解 $\Delta W_{d\times k} = B_{d\times r} A_{r\times k}$,当 r 较小时,A 和 B 的参数量就很小
    • 训练时冻结原来的模型参数 $W$,
    • 随机初始化 $A$ 和 $B$,训练时更新的是 $A$ 和 $B$
    • 最后按照 $W + \Delta W = W + AB$ 来更新参数
  • 需要调整的参数少
  • 还有利于多卡并行训练
  • 因此很快、显存占用也低
    • 具体来说,全参训练相比需要计算全部参数的梯度(显存角度:需要存储全部梯度以及优化器状态);而 LoRA 不需要这些,但是额外需要计算 LoRA 部分的前向、梯度,但这个占比只有1%-3%
  • 推理时,有 sLoRA 这样的技术,使其共享预训练模型参数,进一步降低推理成本。

LoRA 评价

  • 最终效果低于全量微调
  • 如果基座模型表现不好,LoRA 大概率不好(因为只改动了一部分参数)
  • LoRA 记忆力不如全参微调

一个专门解数学题的7B模型训练步骤:

  • step1: 用 Math-500(开源数据集)做 SFT
  • step2: 现价一个难度过滤器,保证每次训练正好是有些难度的
  • step3: 蒸馏超大模型的 CoT
  • step4: 改善 CoT,这个例子的实验表示,CoT 要简繁适中、给出多个思路。

数据量经验值:最少100条,以 5000条为佳

GRPO

区别:

  • SFT:用大模型准备数据,给小模型来训练
  • RL:问题描述(Initial State 无Output),只需要足够多样的问题,无需答案

为什么:“SFT负责记忆,RL负责泛化”

  • SFT Memorizes, RL Generalizes: A Comparative Study of Foundation Model Post-training,https://arxiv.org/abs/2501.17161

大模型相关的 RL 发展历史:

  1. PPO:用了4个模型:Actor Model(大模型本身),Ref Model(冻结的模型),Reward Model,Critic Model(对每个状态的评估,预测未来的潜在回报)
  2. DPO:用一些数学技巧,省去了 Critic Model、Reward Model、Ref Model,只需要 Actor Model 和 accept/reject 。性能很好。Reward 很难写。
  3. GRPO,用组内相对结果来计算奖励,无需 Critic(节省内存)

GRPO(Group Relative Policy Optimization)包含3个模型

  • Actor Model:是训练过程中需要优化的核心策略模型,负责根据输入生成候选输出。通过生成多个输出(如数学题的多解方案)进行群体内对比。
  • Reward Model:用于评估Actor生成输出的质量,为每个输出分配奖励值。通过群体归一化(如减去均值、除以标准差)将绝对奖励转换为相对优势,替代传统优势函数计算。
    • 这个部分也可以是个规则,而不是 Model,例如:
    • 格式奖励(例如是否有think 和 answer,例如格式是否符合预期)
    • 结果正确性奖励。有些问题可以用规则判断
    • 其它奖励:
      • 语言一致性
      • 长度奖励
      • 重复性惩罚
      • 思考过程奖励,条理性、逻辑性
  • Reference Model:是一个冻结(参数不更新)的基准模型,用于计算KL散度(Kullback-Leibler Divergence),约束Actor Model的策略更新幅度,防止其过度偏离初始策略。

GRPO的算法流程

  1. 分组采样响应。对同一个 Prompt 生成一组响应(4-8个)
  2. 组内评分语排名。根据Reward Model(或规则),对每个响应评分,然后,相对优势=个体得分-组平均分
  3. 策略优化方向。强化学习得分相对优势高的,抑制相对优势低的。

Post-Pretrain

使用大量数据,在一个已经训练好的模型上继续训练。

举例:有一个英文的大模型,想做一个中文+金融大模型。做法:

:caption: Post-Pretrain

Post-Pretrain 和 SFT 的区别

  • SFT 主要是问答格式的语料,Post-Pretrain 是普通的语料
  • loss 有对应的调整

性能与资源

一般结论:推理需要的显存为模型大小的2~3倍;后训练需要8~10倍;LoRA 后训要20%这个量级。

影响因素

  • 模型本身的变量个数,例如 Qwen-14B 有 14Billion 个参数;Qwen-7B 有 7Billion 个参数
  • 变量类型:Int8 需要 1个字节;FP16/BF16 需要2个字节;FP32 需要4个字节
    • 推理时用小精度:叫做 量化
    • 最常用的:BF16(范围大、精度小)
    • 游戏卡可以跑大模型,不能做工业渲染
  • 推理 假设参数量为 X,用 BF16,全部参数占用 2X,KV缓存留 20%,其它损耗留 10%,共 2 * 1.3 = 2.6(经验值是 2~3 倍)
  • 训练 参数 2X,梯度 2X,优化器 Adam 4X、激活和损耗 30%,2X(1 + 1 + 2 + 0.3) = 8.6X

推理性能指标

  • 首字延迟(TFFT)。模型开始计算,到产生第一个 Token 消耗的时间。
    • 这个时间内,大模型会把输入的 Prompt 逐 Token 计算一边,并建立 KV Cache
    • 通常是秒级
  • 生成速度(TPOT)。之后的 Token 生成速度。
    • 通常是几十毫秒级
  • QPS

性能提高技术

  • 并行。这里有很多技术
    • 并行训练,多个batch并行处理,然后汇聚梯度
    • 把大矩阵乘法分解到多个卡上并行,计算速度提升,但通信开销变大。
    • 基础工具:PyTorch Distributed、DeepSpeed、通信协议 NCLL(NVIDIA Collective Communications Library,为 GPU 优化的通信库,是 Pytorch 的基础之一)
  • 用 BP16,而不是 FP32
  • prefix cache
    • 适用于 query 有共同的前缀。例如系统提示词都一样。
  • continuous batch:按 batch,把一批需要预测的放到一起。(类似向量化运算)
  • 量化(Quantization) 学术界非常多的方法提出
    • 例如,FP32 转 INT8
  • 剪枝(Pruning):去除接近0的权重
  • 稀疏化。
    • 压缩输入,只选取重要的 Token(gemfilter);
    • 压缩 MLP 计算(对 FNN 模块做剪枝)
    • 压缩 attention (SeerAttention)
  • 一些简单的任务,路由到更小的模型
  • 如果模型只处理特点领域的任务,考虑 distill
  • 大小模型配合(相关技术复杂)

大模型评估

测评大模型(A Survey on Evaluation of Language Models)https://arxiv.org/abs/2307.03109

  • 验证性能和能力
  • 评估泛化能力:在未见过的数据上也保持良好
  • 明确不同的大模型差异:了解其优势领域
  • 防止大模型风险:安全漏洞之类的
  • 知道大模型改进:识别模型弱点、提供优化方向

一些知识点

  • 有时候,多一个换行符,可能对结果产生较大影响

测评方法

  • 在有标准答案的数据集上评测:如何提取模型的答案
  • 针对选择题,计算每个选项的 PPL
  • 对抗测评:在另一个模型上得到回答,比较 A 回答 和 B 回答的好坏。解决的问题:有时候单纯看答案,不能很好的标注回答的好坏。
    • 与基线模型对抗
    • 多个模型两两对抗

评估问题

  • 封闭式问题:使用预先已经有的答案完成测评
  • 开放式问题:
    • 人工测评
    • 大模型测评

安全评估

  • 内生安全:在数据和模型层面的安全
    • 数据投毒:操纵训练样本,使 LLM 在特定触发条件下输出有害结果
    • 模型幻觉。诸如医疗、金融、法律场景可能造成严重后果。主要靠 RAG、MCP
      • 引证生成:要求模型标记来源
        • 方式1:inline citations,用 prompt 约束:“每句必须带引用”、“只允许引用给定 sources 列表”、“引用必须来自支持该句的原文,不支持就说不知道”。缺点是仍然会有幻觉
        • 方式2:post-hoc attribution,先让模型生成不带引用的答案,对每个 claim 做证据匹配(1. 用 embedding 来匹配,2. 用 LLM 来匹配)。更可控,但工程复杂、耗时高
      • self-RAG:由 LLM 自动决定是否检索、检索什么、如何使用证据、何时停止
    • 价值观问题
  • 服务安全:AI+、智能体、各种服务的漏洞
    • 数据泄露(用户隐私、提示词之类的)
    • 安全攻击(类比传统的互联网攻击)
    • 供应链风险
  • 使用上的安全、未来的安全问题
    • 伪造
    • 虚假新闻
    • 错误的重大建议(医疗、金融)

评估的板块

  • 内容安全:黄赌毒、迷信、恐怖暴力
  • 意识形态
  • 数据安全
  • 伦理:身心健康、公序良俗、礼貌、防歧视
  • 合规:符合国家的部门规章(金融、医疗、政务等)
  • 自我认知:xx公司开发的xx模型
  • 真实性

攻击手法

  • 指令劫持
    • 口令复述
    • 正反介绍
    • 循序渐进
    • 情景带入
  • 绕过检测
    • 内涵
    • 藏头诗
    • 嵌套

模型水印

  • 模型文件水印、AIGC标识
  • 参数级水印
  • 行为级水印:特定输入,使其呈现特殊的输出
  • 数据水印:“善意”版本的数据投毒

一些 AI 产品架构

一、转发回复架构

:caption: 转发回复架构

这个是最简单的架构,常用于聊天 app

分拆任务架构

graph TB A[用户请求] B[前端] subgraph S[ ] direction LR C[后端] E1((模型1)) E2((模型2)) E3((模型3)) D[数据库] end A --> B B --> A B --> C C --> B C --> D D --> C C --> E1 E1 --> C C --> E2 E2 --> C C --> E3 E3 --> C

举例:

  • 论文阅读助手,会有多个模型,做不同的任务
  • AI PPT,有的模型规划大纲,有的模型画结构,有的模型填内容

三、事件触发

graph TB A[其它系统] D[动作] A --->|触发(事件、定时)| B subgraph S[ ] direction LR B[后端系统] C[Agent] end B --> C C --> D

例子:

  • 新闻订阅
  • 金融事件响应系统
  • 市场调研助手:问卷收集到一定数量后,启动流程,最终给用户输出报告

感知启动

:caption: 感知启动

例子:法律咨询助手

  • Agent:观察对话、理解要点、判断潜在需求、匹配类似案件等

其它:以上架构的组合

多Agent

整体思路

  • 角色分解:
    • supervisor:只负责需求拆解、分发任务,没有执行权限
    • 执行者:负责执行具体的任务
    • SOP标准化
  • 人设隔离,划分不同的人设,不同的人可使用不同的工具集
    • 人设:“你是一个资深Java开发,只负责开发后端代码”
    • 工具:例如检索、Python解释器、数据库读写
  • 通信机制
    • 点对点传递:适合线形任务
    • 共享黑板:全局共享当前状态,适合复杂任务
  • 协作模式
    • 层级式:Boss -> Leader -> Worker
      • 中心化调度
      • 效率高
    • 辩论式: Agent A VS Agent B
      • 多轮互斥讨论和 review,准确率高
    • 流水线:固定顺序执行,适合 SOP 明确的任务

问题

  • 错误叠加,整体出错概率极高
  • 蝴蝶效应,错误传导
  • 解决方案:
    • 断点续传
    • agent判断调用错误状态,并自动行动

工程经验

  • 多 Agent 不能互相耦合。
    • 否则:风格(思路)不统一,即使它们共享上下文;
    • 决策与执行尽量用同一个模型。
    • 可以拆出来的:例如一个子Agent去查询、学习一个生僻的库,而绝不能用它来写代码。

一些方向

text2sql

流程

  1. 用户问题
  2. 构建 prompt
    • 选择 schema
    • 构建的 Prompt 要求:简洁、严格规定不准自由发挥、不准解释,可以添加 few shot
  3. 生成 SQL
    • 用一个单独的 LLM 来执行
    • 可以生成 N 份,然后让模型选择最优的
  4. 校验
    • 规则校验
    • 模型校验
  5. 执行
    • 如有报错,则返回给大模型
  6. (如有必要)解释结果,结构化数据转文本


您的支持将鼓励我继续创作!