Skip to content

LLM

12 posts with the tag “LLM”

LLM temerature 和 top_p

Temperature(温度)和 Top_p(核采样)是控制 LLM 输出随机性创造力的两个核心参数。

简单来说:Temperature 调整概率分布的形状,而 Top_p 则是切断低概率的选项。

以下是详细的选择指南和最佳实践:

  • Temperature (0.0 - 2.0):

  • 作用:控制“大胆程度”。

  • 低温 (Low):模型变得保守、自信,倾向于选择概率最高的词。输出稳定、重复。

  • 高温 (High):模型变得“疯狂”,会给低概率词更多的机会。输出多样、不可预测。

  • Top_p (0.0 - 1.0):

  • 作用:控制“候选词范围”。

  • 低 Top_p:只从前 X% 概率最高的词里选(例如只看前 10% 的词)。

  • 高 Top_p:允许从更广泛的词汇库中选择。


这是最实用的速查表,根据你的任务类型进行选择:

任务场景Temperature 推荐Top_p 推荐目标效果
代码生成 / 数学解题0.0 - 0.20.1 - 0.2精确、确定性。避免模型瞎编乱造,保证逻辑严密。
数据提取 / 格式转换0.00.1严格。你需要 JSON 就是 JSON,不要多余的废话。
通用问答 / 客服0.5 - 0.70.8 - 0.9平衡。既要准确,又要像人说话一样自然,不呆板。
文章润色 / 摘要0.5 - 0.70.9流畅。保留原文意思,但在措辞上有一点灵活性。
创意写作 / 头脑风暴0.8 - 1.0+0.9 - 1.0发散。需要模型给出意想不到的点子,允许偶尔的“胡言乱语”。

3. 如何协同调整?(黄金法则)

Section titled “3. 如何协同调整?(黄金法则)”

大多数 API 提供商(如 OpenAI)建议遵循一个原则:通常只调整其中一个,而不是同时调整两个。

策略 A:锁定 Top_p,只调 Temperature(推荐)

Section titled “策略 A:锁定 Top_p,只调 Temperature(推荐)”

这是最常见的做法。

  • Top_p 设置为 1.0(即不限制候选池)。
  • 通过 Temperature 来控制整体的随机性。
  • 写代码?Temp = 0.1
  • 写小说?Temp = 0.9

策略 B:锁定 Temperature,只调 Top_p

Section titled “策略 B:锁定 Temperature,只调 Top_p”
  • Temperature 设置为 1.0(标准分布)。
  • 通过 Top_p 来切断长尾词。
  • 想要极度稳健?Top_p = 0.1
  • 想要多样性?Top_p = 0.95

4. 深入理解:它们到底有什么区别?

Section titled “4. 深入理解:它们到底有什么区别?”

为了形象地理解,假设模型在预测下一个词,候选词及其概率如下: [苹果: 50%, 香蕉: 30%, 飞船: 15%, 恐龙: 5%]

  • 调节 Temperature (例如调高)

  • 它会“拉平”概率分布。

  • 苹果的优势变小(变成 40%),恐龙的机会变大(变成 15%)。

  • 结果:所有词都有机会被选中,甚至是很离谱的词。

  • 调节 Top_p (例如设为 0.8)

  • 模型只取累计概率达到 80% 的前几个词。

  • 50% (苹果) + 30% (香蕉) = 80%。

  • 结果飞船恐龙直接被切掉了,根本不参与随机。模型只能在苹果香蕉里选。

  • 如果不确定:默认保持 Top_p = 1.0,只调整 Temperature
  • 要精准Temperature 设为 0。
  • 要创意Temperature 设为 0.8 ~ 1.0。

LlamaCoder:开源版 Claude Artifacts 的全链路流程解析

LlamaCoder 是一个由 Nutlope 开发的开源项目,旨在利用 Llama 3.1 等大语言模型实现类似 Claude Artifacts 的代码生成与实时预览功能。本文将深入剖析 LlamaCoder 的核心工作流程,包括会话创建、多模型协作以及代码生成的具体实现逻辑,帮助读者理解其背后的技术架构与设计思路。

功能概述: 初始化一个新的 AI 编码对话会话

核心流程:

用户提交 prompt
1. 创建 Chat 记录(数据库)
2. 并行执行两个任务:
├── fetchTitle() - 生成聊天标题(3-5词)
└── fetchTopExample() - 匹配相似示例(landing page/blog app等)
3. 如果有截图 → 调用视觉模型分析截图
4. 如果 quality === "high" → 调用架构模型生成项目计划
5. 保存初始消息到数据库:
├── System 消息(编码指令)
└── User 消息(prompt 或项目计划)
6. 返回 { chatId, lastMessageId }

使用的模型:

  • meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo - 标题生成、示例匹配
  • Qwen/Qwen3-VL-32B-Instruct - 截图分析
  • Qwen/Qwen3-Next-80B-A3B-Instruct - 项目计划

2. /api/get-next-completion-stream-promise - 流式生成代码

Section titled “2. /api/get-next-completion-stream-promise - 流式生成代码”

功能概述: 根据对话历史流式生成 AI 响应

核心流程:

接收 messageId, model
1. 查询消息及历史记录(position <= 当前消息)
2. Token 优化:
├── optimizeMessagesForTokens() - 移除早期 assistant 消息中的代码块
└── 消息长度限制 - 超过10条时保留 [前3条 + 最后7条]
3. 调用 LLM(stream: true)
4. 直接返回流式响应

Token 优化策略:

  1. 代码块剥离 - 保留最后 2 条 assistant 消息的完整内容,早期消息删除代码块
  2. 消息裁剪 - 最多保留 10 条消息:[0, 1, 2] + 最后7条
[page.tsx:132-167]
用户提交表单
POST /api/create-chat
(创建会话、生成标题、项目计划)
返回 { chatId, lastMessageId }
POST /api/get-next-completion-stream-promise
(核心:这里调用 LLM stream: true)
获取流式响应 → 跳转到 /chats/{chatId}
(页面边接收边显示 AI 生成的代码)

简单总结:

接口作用返回
/api/create-chat初始化对话,生成标题、项目计划chatIdlastMessageId
/api/get-next-completion-stream-promise流式生成 AI 代码响应可读流

页面跳转到 /chats/{chatId} 时的完整流程

Section titled “页面跳转到 /chats/{chatId} 时的完整流程”
[首页] 用户提交表单
POST /api/create-chat
→ 创建 Chat 记录
→ 生成初始消息(system + user)
→ 返回 { chatId, lastMessageId }
POST /api/get-next-completion-stream-promise
→ 获取流式响应 Promise
router.push(`/chats/${chatId}`)
┌─────────────────────────────────────────────────┐
│ [chats/[id]/page.tsx] 服务端渲染 │
├─────────────────────────────────────────────────┤
│ 1. getChatById(id) │
│ → 从数据库加载 Chat 和消息 │
│ 2. 生成 HTML + 初始数据 │
│ 3. 返回给客户端 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ [page.client.tsx] 客户端激活 │
├─────────────────────────────────────────────────┤
│ 1. 接收服务端传来的 chat 数据 │
│ 2. 从 Context 获取 streamPromise │
│ 3. useEffect 触发流处理 │
│ 4. 开始接收 AI 流式响应 │
│ 5. 逐字显示代码 │
│ 6. 流结束后保存消息到数据库 │
└─────────────────────────────────────────────────┘
// 用户访问 /chats/abc123
export default async function Page({ params }) {
const id = (await params).id;
// 从数据库加载聊天数据
const chat = await getChatById(id);
// {
// id: "abc123",
// title: "Todo App",
// messages: [
// { position: 0, role: "system", content: "..." },
// { position: 1, role: "user", content: "Build me a todo app" }
// ]
// }
return <PageClient chat={chat} />;
}

此时 AI 还没有生成代码,只有初始的 system 和 user 消息。


export default function PageClient({ chat }: { chat: Chat }) {
const context = use(Context);
// streamPromise 是从首页传过来的!
const [streamPromise, setStreamPromise] = useState(
context.streamPromise // ← 这是已经开始的流
);

useEffect(() => {
async function f() {
const stream = await streamPromise; // 获取可读流
ChatCompletionStream.fromReadableStream(stream)
.on("content", (delta, content) => {
// 每收到一个 token 触发
setStreamText((text) => text + delta);
// "im" -> "imp" -> "impor" -> "import" ...
// 检测到代码时显示代码查看器
if (content.includes("```")) {
setIsShowingCodeViewer(true);
}
})
.on("finalContent", async (finalText) => {
// 流结束后保存到数据库
const message = await createMessage(
chat.id,
finalText, // 完整的 AI 响应
"assistant",
allFiles, // 提取的文件
);
router.refresh(); // 刷新服务端数据
});
}
f();
}, [streamPromise]);

时间轴:
T+0s: 页面加载,显示 ChatLog(只有初始消息)
┌─────────────────────────┐
│ system: You are expert │
│ user: Build me a todo │
└─────────────────────────┘
T+1s: 开始接收流
┌─────────────────────────┐
│ system: You are expert │
│ user: Build me a todo │
│ assistant: Here's a │ ← 实时显示
└─────────────────────────┘
T+2s: 继续接收
│ assistant: Here's a React│
T+5s: 检测到代码块,打开 CodeViewer
┌────────────┬─────────────┐
│ ChatLog │ CodeViewer │
│ ... │ import React│
│ │ export ... │
└────────────┴─────────────┘
T+30s: 流结束,保存到数据库
router.refresh() → 服务端数据更新

阶段位置发生了什么
首页page.tsx调用 /api/create-chat → 创建 Chat
首页page.tsx调用 /api/get-next-completion-stream-promise → 获取流
跳转router.push导航到 /chats/{id},携带 streamPromise
服务端[id]/page.tsxgetChatById() → 加载数据库数据(此时无 AI 响应)
客户端page.client.tsxuseEffect → 处理流,逐字显示
结束page.client.tsxcreateMessage() → 保存 AI 响应到数据库

关键理解: 页面跳转时,AI 已经在生成代码了(流已经开始),页面只是负责接收和显示这个流。

这是一个服务端数据获取函数,用于加载聊天会话的完整数据。

const getChatById = cache(async (id: string) => {
// 1. 查询 Chat 基本信息
// 2. 查询消息(分批加载策略)
// 3. 返回组合数据
});
1. 查询 Chat 记录
2. 统计总消息数 (totalMessages)
3. 分批加载消息:
├── position 0,1 (必须加载:system prompt + 初始用户消息)
└── position ≥2 的最近 100 条
4. 计算版本计数器 (assistantMessagesCountBefore)
5. 返回组合数据
假设数据库有 200 条消息:
position 0: [system] 编码指令
position 1: [user] 初始 prompt
position 2-101: [对话...]
position 102-200: [最近对话...]
加载结果:
✓ position 0 (必须)
✓ position 1 (必须)
✓ position 102-200 (最近 100 条)
✗ position 2-101 (跳过,节省数据传输)
最终按 position 排序返回
const getChatById = cache(async (id: string) => {

React Server Component 的缓存机制:

同一个请求中,getChatById 可能被调用多次:
├── generateMetadata() 需要 chat.title
└── Page() 需要 chat 数据
使用 cache() 确保只查询一次数据库,
第二次调用直接返回缓存结果
{
id: "abc123",
title: "Budgeting App",
model: "deepseek-v3-2-251201",
quality: "high",
prompt: "Build me a budgeting app",
messages: [
{ position: 0, role: "system", content: "..." },
{ position: 1, role: "user", content: "..." },
{ position: 102, role: "assistant", content: "..." },
// ... 最多 103 条消息
],
totalMessages: 200, // 总消息数(用于显示"加载更多")
assistantMessagesCountBefore: 50 // 用于版本控制
}
  1. 性能 - 不加载所有历史消息,只加载必要的 + 最近 100 条
  2. 成本 - 减少数据库查询和数据传输
  3. 用户体验 - 用户主要关心最近的对话内容

流式响应中提取代码到多个文件的完整解析

Section titled “流式响应中提取代码到多个文件的完整解析”

AI 被要求以特定的 Markdown 格式输出多个文件:

Here's your todo app:
```tsx{path=src/App.tsx}
import React from 'react';
export default function App() { ... }
body { margin: 0; }
{
"name": "todo-app"
}
### 2. 代码块提取 (`lib/utils.ts`)
#### `parseReplySegments()` - 实时解析流式内容
```typescript
// 逐行解析,支持流式(未闭合的代码块标记为 partial)
export function parseReplySegments(markdown: string): ReplySegment[] {
const lines = markdown.split("\n");
const fenceRegex = /^```([^\n]*)$/;
let openTag: string | null = null; // 当前代码块是否打开
let codeBuffer: string[] = []; // 代码内容缓冲区
for (const line of lines) {
const match = line.match(fenceRegex);
if (match && !openTag) {
// 开始代码块: tsx{path=src/App.tsx}
openTag = match[1] || "";
} else if (match && openTag) {
// 结束代码块: ```
segments.push({
type: "file",
code: codeBuffer.join("\n"),
language: "tsx",
path: "src/App.tsx",
isPartial: false, // ← 已完成
});
openTag = null;
} else if (openTag) {
// 代码块内的行
codeBuffer.push(line);
}
}
// 如果流结束时代码块未闭合
if (openTag) {
segments.push({
type: "file",
code: codeBuffer.join("\n"),
language: "tsx",
path: "src/App.tsx",
isPartial: true, // ← 正在生成中
});
}
}

extractAllCodeBlocks() - 提取所有已完成的代码块

Section titled “extractAllCodeBlocks() - 提取所有已完成的代码块”
export function extractAllCodeBlocks(input: string) {
const codeBlockRegex = /```([^\n]*)\n([\s\S]*?)\n```/g;
const files = [];
let match;
while ((match = codeBlockRegex.exec(input)) !== null) {
const fenceTag = match[1]; // "tsx{path=src/App.tsx}"
const code = match[2]; // 代码内容
// 解析标签
const { language, path } = parseFenceTag(fenceTag);
// language = "tsx"
// path = "src/App.tsx"
files.push({ code, language, path });
}
return files;
}
function parseFenceTag(tag: string) {
// 输入: "tsx{path=src/App.tsx}"
const langMatch = tag.match(/^([A-Za-z0-9]+)/);
const language = langMatch ? langMatch[1] : "text";
// language = "tsx"
const pathMatch = tag.match(/path\s*=\s*([^}\s]+)/);
const path = pathMatch ? pathMatch[1] : `file.${getExtensionForLanguage(language)}`;
// path = "src/App.tsx"
return { language, path };
}
ChatCompletionStream.fromReadableStream(stream)
.on("content", (delta, content) => {
// 每收到一个 token
setStreamText((text) => text + delta);
// 解析当前内容
const segments = parseReplySegments(content);
// [
// { type: "text", content: "Here's your app:\n\n" },
// { type: "file", code: "import...", language: "tsx", path: "src/App.tsx", isPartial: true }
// ]
// 检测到文件时打开代码查看器
if (segments.some((seg) => seg.type === "file")) {
setIsShowingCodeViewer(true);
}
// 第一个完整文件时显示预览
if (segments.some((seg) => seg.type === "file" && !seg.isPartial)) {
setActiveTab("preview");
}
})
.on("finalContent", async (finalText) => {
// 流结束后提取所有文件
const currentFiles = extractAllCodeBlocks(finalText);
// [
// { code: "import React...", language: "tsx", path: "src/App.tsx" },
// { code: "body { margin: 0 }", language: "css", path: "src/styles.css" },
// { code: "{ \"name\": \"todo\" }", language: "json", path: "package.json" }
// ]
// 合并之前的文件(同一 path 的文件会被覆盖)
const fileMap = new Map();
previousFiles.forEach((f) => fileMap.set(f.path, f));
currentFiles.forEach((f) => fileMap.set(f.path, f));
const allFiles = Array.from(fileMap.values());
// 保存到数据库
await createMessage(chat.id, finalText, "assistant", allFiles);
});
// 合并流中的文件
const streamAllFiles = extractAllCodeBlocks(streamText); // 已完成的
const latestStreamBlock = extractLatestStreamBlock(streamText); // 正在生成的
// 合并:同一 path 的文件,新的覆盖旧的
let mergedStreamFiles = [...streamAllFiles];
if (latestStreamBlock) {
const existingIdx = mergedStreamFiles.findIndex(
(f) => f.path === latestStreamBlock.path,
);
if (existingIdx !== -1) {
mergedStreamFiles[existingIdx] = latestStreamBlock; // 更新正在生成的文件
} else {
mergedStreamFiles.push(latestStreamBlock); // 添加新文件
}
}
// 与之前消息的文件合并
const baseFiles = lastMessage ? getFilesFromMessage(lastMessage) : [];
const files = mergeFiles(baseFiles, mergedStreamFiles);
AI 流式输出:
"Here's your app:\n\n```tsx{path=App.tsx}\nimpor" (t+1s)
"Here's your app:\n\n```tsx{path=App.tsx}\nimport" (t+2s)
"Here's your app:\n\n```tsx{path=App.tsx}\nimport React" (t+3s)
...
"Here's your app:\n\n```tsx{path=App.tsx}\nexport default" (t+10s)
"Here's your app:\n\n```tsx{path=App.tsx}\nexport default\n```\n\n" (t+11s - 文件1完成)
"```css{path=styles.css}\nbody" (t+12s - 开始文件2)
...
实时解析:
t+1s: [{ type: "file", path: "App.tsx", isPartial: true, code: "impor" }]
t+10s: [{ type: "file", path: "App.tsx", isPartial: true, code: "export default" }]
t+11s: [{ type: "file", path: "App.tsx", isPartial: false, code: "export default" }] ← 完成
t+12s: [{ type: "file", path: "App.tsx", isPartial: false },
{ type: "file", path: "styles.css", isPartial: true }]
UI 显示:
┌─────────────────────────────────┐
│ 📁 App.tsx │
│ import React; │
│ export default function App() { │ ← 实时更新
│ return <div>Hello</div>; │
│ } │
├─────────────────────────────────┤
│ 📁 styles.css (generating...) │
│ body │
└─────────────────────────────────┘
函数作用时机
parseReplySegments()实时解析,包括 partial 文件每次收到 token
extractAllCodeBlocks()提取所有已完成的代码块流结束后/查看历史
parseFenceTag()解析 tsx{path=src/App.tsx}解析代码块标签
extractLatestStreamBlock()获取当前正在生成的代码块实时显示

StickToBottom 包裹 SyntaxHighlighter 的原因

Section titled “StickToBottom 包裹 SyntaxHighlighter 的原因”

StickToBottom 是一个组件,用于在内容动态增长时自动滚动到底部。这对于流式代码生成非常重要。

AI 正在生成代码(流式输出):
第1秒: import React from 'react';
第2秒: import React from 'react';
export default function App() {
第3秒: import React from 'react';
export default function App() {
return <div>Hello
第4秒: import React from 'react';
export default function App() {
return <div>Hello World</div>;
}

如果不自动滚动:

初始视图(可见第1-20行):
┌─────────────────────────────┐
│ import React from 'react'; │ ← 第1行(可见)
│ ... │
│ const data = [ │ ← 第20行(可见)
└─────────────────────────────┘
第5秒后(AI生成了50行代码):
┌─────────────────────────────┐
│ import React from 'react'; │ ← 第1行(仍然可见)
│ ... │
│ const data = [ │ ← 第20行(仍然可见)
│ │
│ ↑ 用户看不到正在生成的代码! │
└─────────────────────────────┘
新增的第21-50行在视野外

使用 StickToBottom 后:

第5秒后(自动滚动到底部):
┌─────────────────────────────┐
│ ... │
│ const data = [ │
│ { id: 1, name: 'A' }, │
│ ]; │
│ │
│ export default function App()│ ← 新生成的代码(可见)
│ return <div>Hello │ ← 用户看到实时生成!
└─────────────────────────────┘
自动滚动到最后
code-viewer.tsx
<StickToBottom
className="relative grow overflow-hidden *:!h-[inherit]"
resize="smooth"
initial={false}
>
<StickToBottom.Content>
<SyntaxHighlighter
files={files.map((f) => ({
path: f.path,
content: f.code,
language: f.language,
}))}
activePath={
streamText
? latestStreamBlock?.path || files.at(-1)?.path
: undefined
}
disableSelection={!!streamText}
isStreaming={!!streamText}
/>
</StickToBottom.Content>
</StickToBottom>

StickToBottom 的工作机制:

  1. 监听内容高度变化
  2. 当内容增长时,自动滚动到底部
  3. resize="smooth" - 平滑滚动动画
  4. initial={false} - 初始不滚动(只有内容变化时才滚动)
// SyntaxHighlighter 内部也有自动滚动逻辑
useEffect(() => {
if (!isStreaming || !editorRef.current) return;
const editor = editorRef.current;
const lineCount = model?.getLineCount?.() || 1;
// 滚动到最后
editor.revealLine?.(lineCount);
editor.setScrollTop?.(scrollHeight);
}, [file?.content, activeFile, isStreaming]);
层级作用范围
StickToBottom滚动整个代码查看器容器外层容器滚动
Monaco Editor滚动编辑器内容到最后一行编辑器内部滚动
┌─────────────────────────────────────────┐
│ StickToBottom (外层容器) │
│ ┌─────────────────────────────────────┐ │
│ │ Monaco Editor (内层编辑器) │ │
│ │ │ │
│ │ import React; │ │
│ │ ... │ │
│ │ export default... (生成中) │ │ ← 两者协同确保这里可见
│ │ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
无 StickToBottom:
[ 代码生成中... ]
│ 第1行 │
│ 第2行 │
│ 第3行 │ ← 用户手动滚动
...
│ 第50行 │ ← 用户需要手动滚动到这里才能看到新代码
有 StickToBottom:
[ 代码生成中... ]
...
│ 第48行 │ ← 自动跟随
│ 第49行 │ ← 自动跟随
│ 第50行 │ ← 自动跟随,用户始终看到最新生成的代码

StickToBottom 确保在 AI 流式生成代码时:

  1. 用户始终看到最新代码 - 自动滚动到正在生成的位置
  2. 无需手动滚动 - 提供更好的观看体验
  3. 平滑动画 - resize="smooth" 提供流畅的视觉效果
  4. 只在流式时启用 - isStreaming 控制是否启用

LLM分词和嵌入

分词器负责在将输入文本送入生成模型之前,将其分割成词元。

下面使用 transformers 来加载分词器和模型。Hugging Face 网站上找到分词器和模型, 只需传入相应的 ID 即可。使用 microsoft/Phi-3- mini-4k-instruct 作为模型的主路径,这里使用的是 Google Colab 提供的 T4 GPU(device_map=“cuda”),也可以使用其他设备。

  • 加载依赖
!pip install --upgrade transformers==4.41.2 sentence-transformers==3.0.1 gensim==4.3.2 scikit-learn==1.5.0 accelerate==0.31.0 peft==0.11.1 scipy==1.10.1 numpy==1.26.4
  • 下载和运行LLM
from transformers import AutoModelForCausalLM, AutoTokenizer
# 加载模型和分词器
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Phi-3-mini-4k-instruct",
device_map="cuda",
torch_dtype="auto",
trust_remote_code=False,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")
  • 首先声明提示词

然后对其进行分词,再将这些词元传递给模型,模型随后生成输出。

prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened.<|assistant|>"
# Tokenize the input prompt
input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to("cuda")
# Generate the text
generation_output = model.generate(
input_ids=input_ids,
max_new_tokens=20
)
# Print the output
print(tokenizer.decode(generation_output[0]))
The attention mask is not set and cannot be inferred from input because pad token is same as eos token. As a consequence, you may observe unexpected behavior. Please pass your input's `attention_mask` to obtain reliable results.
Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened.<|assistant|> Subject: Heartfelt Apologies for the Gardening Mishap
Dear
  • 模型实际上并没有直接处理提示词文本。相反,输入提示词是由分词器处理的。分词器在变量 input_ids 中返回了模型所需的信息,随后模型将其用作输入。
  • 分词器在变量 input_ids 中返回了模型所需的信息,随后模型将其用作输入。

打印 input_ids 可以看到:

print(input_ids)
  • output
tensor([[14350, 385, 4876, 27746, 5281, 304, 19235, 363, 278, 25305,
293, 16423, 292, 286, 728, 481, 29889, 12027, 7420, 920,
372, 9559, 29889, 32001]], device='cuda:0')
for id in input_ids[0]:
print(tokenizer.decode(id))
  • output
Write
an
email
apolog
izing
to
Sarah
for
the
trag
ic
garden
ing
m
ish
ap
.
Exp
lain
how
it
happened
.
<|assistant|>
  • 每个整数都是特定词元(字符、词或词的一部分)的唯一 ID。这些 ID 是分词器内部的一张词元表的索引,该表包含了分词器能够识别的所有词元;
  • 第一个词元是 ID 1(),这是一个表示文本开始的特殊词元;
  • 一些词元是完整的单词(例如 Write、an、email);
  • 一些词元是单词的部分(例如 apolog、izing、trag、ic);
  • 标点符号是独立的词元。
  • 注意空格字符不用单独的词元表示,代表词的一部分的词元(如 izing 和 ic)在开头有一个特殊的隐藏字符,表示它们与文本中前面的词元相连。没有这个特殊字符的词元前面则都被视为有一个空格。

使用 `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)

向量归一化和相似度

在机器学习和深度学习中,对嵌入向量进行归一化(Normalization)至关重要,它能带来多方面的益处,尤其是在涉及到相似度计算、模型训练和数据表示时。归一化通常指的是将向量的长度(或范数)缩放为单位长度(通常为 L2 范数等于 1)。以下是归一化的主要必要性:

  • 避免特征尺度差异的影响: 不同的特征或模型生成的原始嵌入向量可能具有不同的尺度和范围。例如,一个模型的嵌入向量的数值可能在 -1 到 1 之间,而另一个模型的可能在 -100 到 100 之间。如果不进行归一化,在计算向量之间的距离或相似度时(如欧氏距离、点积),尺度较大的向量会占据主导地位,导致模型更关注这些尺度较大的特征,而忽略了尺度较小的特征中可能包含的重要信息。
  • 确保每个特征的贡献相等: 归一化可以将所有嵌入向量的元素缩放到相似的尺度,使得每个维度在距离和相似度计算中具有更公平的权重,从而提高模型的鲁棒性和泛化能力。
  • 提高余弦相似度的准确性: 余弦相似度是衡量两个向量方向之间夹角的余弦值,常用于计算文本、图像等嵌入的语义相似性。当向量未归一化时,向量的长度(magnitude)会影响点积的值,从而影响余弦相似度的结果。归一化后,向量的长度变为 1,此时余弦相似度就直接等于向量的点积,能够更准确地反映向量方向上的相似程度。
  • 改善基于距离的度量: 对于依赖欧氏距离等距离度量的算法(如 K-近邻、K-均值聚类),归一化可以确保距离的计算不会被尺度较大的特征所主导,从而得到更符合语义或特征实际分布的结果。
  • 避免梯度爆炸或消失: 在神经网络训练中,如果输入的特征尺度过大或过小,可能导致梯度在反向传播过程中变得非常大(梯度爆炸)或非常小(梯度消失),从而影响模型的收敛速度和训练稳定性。归一化可以将输入特征限制在合理的范围内,有助于缓解这些问题。
  • 加快收敛速度: 当特征具有相似的尺度时,梯度下降等优化算法可以更有效地找到最优解,因为模型不需要在不同尺度的特征空间中来回震荡。
  • 减少模型对异常值的敏感性: 某些归一化方法(如 Z-score 归一化)可以降低异常值对模型的影响,使模型更加稳定。
  • 提升模型在不同数据集上的泛化能力: 通过统一特征的尺度,归一化可以帮助模型学习到更通用的模式,而不是过度依赖于特定数据集的尺度分布。

方便不同嵌入模型的比较和集成:

Section titled “方便不同嵌入模型的比较和集成:”
  • 统一不同模型的输出尺度: 不同的嵌入模型可能产生具有不同尺度范围的向量。归一化可以将它们的输出统一到一个共同的尺度,使得可以直接比较或组合来自不同模型的嵌入,从而进行更复杂的分析或构建更强大的系统。

总结

归一化是处理嵌入向量的一个关键步骤,它可以确保相似度计算的准确性,优化模型训练过程,提高模型性能和泛化能力,并方便不同嵌入之间的比较和集成。在大多数使用嵌入向量的机器学习和深度学习应用中,都强烈建议进行归一化处理。最常用的归一化方法是 L2 归一化,它将向量的长度缩放为单位长度。

相似度量用于衡量向量之间的相似性。选择合适的距离度量有助于显著提高分类和聚类性能。向量的相似度量有:余弦相似度(Cosine Similarity)、内积(Inner Product, IP)、欧氏距离(Euclidean Distance)、曼哈顿距离(Mahalanobis Distance)、余弦距离(Cosine Distance)、汉明距离(Hamming Distance)、Jaccard 相似度(Jaccard Similarity)、BM25 相似度(BM25 Similarity)等等。

Cosine(A, B) = (A · B) / (||A|| * ||B||)。它衡量的是两个向量方向上的一致性,忽略了向量的长度(模长)。它的取值范围是 [-1, 1],越接近 1 表示方向越一致。 例如,两个正比向量的余弦相似度为1,两个正交向量的余弦相似度为0,两个相反向量的余弦相似度为-1,余弦越大,两个向量之间的夹角越小,说明这两个向量之间的相似度越高。 用 1 减去它们的余弦相似度,就可以得到两个向量之间的余弦距离。

IP(A, B) = A · B。它计算的是两个向量的点积。这个值同时受到向量方向和向量模长的影响。如果两个向量方向一致,它们的模长越大,内积通常也越大。IP 的取值范围没有固定界限(取决于向量模长)。

3、同样的数据,内积 (IP) 能查到,但是余弦相似度查询不到

Section titled “3、同样的数据,内积 (IP) 能查到,但是余弦相似度查询不到”

因为内积查询需要计算两个向量的点积,而余弦相似度只需要计算两个向量的余弦值。

$~$

  • 如果向量是归一化的(即模长为1,||A|| = 1||B|| = 1),那么Cosine(A, B) = A · B = IP(A, B)。在这种情况下,使用余弦相似度和内积进行检索,得到的结果排序应该是完全相同的。

  • 如果向量不是归一化的,那么Cosine(A, B) = (A · B) / (||A|| * ||B||),而IP(A, B) = A · B。在这种情况下,使用余弦相似度和内积进行检索,得到的结果排序可能不同。


具体到这个现象来说:


  • 内积 (IP) 成功: 这很可能意味着你想要查找的那个目标数据向量,与你的查询向量之间的点积 (A · B) 相对较高。这个高点积可能是因为它们的方向比较接近,也可能是因为一个或两个向量的模长(长度)非常大,即使方向不是最接近的,较大的模长也导致了较高的内积得分,使其排在了前面。
  • 余弦相似度 (Cosine) 失败: 这表明,虽然目标向量和查询向量的内积可能较高(被 IP 检索到),但当除以各自的模长进行归一化计算后 ((A · B) / (||A|| * ||B||)),得到的余弦相似度得分反而不高。可能有其他向量,虽然它们的模长较小(导致 IP 得分不如目标向量),但它们与查询向量的方向更一致(角度更小),因此具有更高的余弦相似度得分,把真正的目标挤出了 top-k 结果。

总结来说可能原因是:


  • 向量未归一化: 这是最主要的原因。gte-Qwen2-7B-instruct 或你使用的其他嵌入模型产生的向量可能其模长不为 1。
  • 模长的影响: 能够被 IP 检索到的目标数据,很可能与查询向量的点积较大,这其中较大的模长起到了重要作用,弥补了方向上可能不是绝对最优的劣势。
  • 方向 vs. 模长: 其他数据可能在方向上与查询向量更接近(余弦相似度更高),但由于模长较小,其内积得分不如目标数据高。

取一个由模型产生的向量 v,计算其 L2 范数(模长):norm = np.linalg.norm(v)。如果 norm 不接近 1,那么向量就没有归一化。


检查向量是否归一化,可以使用以下代码:
from llama_index.embeddings.ollama import OllamaEmbedding
import numpy as np
def is_normalized(vector):
return np.isclose(np.linalg.norm(vector)1.0
def normalize(vector):
if is_normalized(vector):
return vector
return vector / np.linalg.norm(vector)
def normalize_vectors(vectors):
return [normalize(vector) for vector in vectors]
embedd = OllamaEmbedding(
# model_name="Embedding-GGUF/gte-Qwen2-1.5B-instruct-Q4_K_M-GGUF",
model_name="modelscope.cn/Embedding-GGUF/gte-Qwen2-7B-instruct-Q4_K_M-GGUF:latest",
base_url="http://localhost:11434",
normalize=True
)
embedding = embedd.get_text_embedding("hello")
print(f"向量是否归一化: {is_normalized(embedding)}")

RAG vs 模型微调

RAG (Retrieval-Augmented Generation),中文可以理解为检索增强生成,是一种通过从外部知识库检索信息来增强大型语言模型(LLM)能力的AI框架。


简单来说,传统的 LLM 是基于其训练数据来生成文本的,而 RAG 则在此基础上增加了一个步骤:当用户提出问题或指令时,RAG会先从外部的信息源(比如网页、数据库、文档等)中搜索相关的知识,然后将这些检索到的信息与用户的问题一起提供给 LLM,让 LLM 在生成答案时能够参考这些外部信息


RAG的主要步骤可以概括为:


  1. 检索 (Retrieval): 接收用户的查询,并从外部知识库中找到与之相关的信息。这通常涉及到信息检索技术,例如关键词搜索、向量搜索等。
  2. 增强 (Augmentation): 将检索到的相关信息与用户的原始查询结合起来,形成一个更丰富的输入(通常称为提示,Prompt),提供给 LLM。
  3. 生成 (Generation): LLM 接收到增强后的输入,并基于这些信息生成最终的回答或文本。

使用RAG的好处包括:


  1. 获取最新的信息: LLM的训练数据通常是静态的,而RAG可以实时地从外部获取最新的信息,从而生成更准确、及时的回答。
  2. 减少“幻觉”: 通过引用外部的可靠信息来源,RAG可以降低LLM生成不真实或不相关信息的风险。
  3. 提高透明度: 在某些情况下,RAG可以提供生成答案所依据的外部信息来源,帮助用户验证答案的可靠性。
  4. 降低计算成本: 相比于重新训练整个LLM来使其掌握新知识,RAG的成本通常更低。
  5. 适应特定领域: RAG可以连接到特定的知识库,例如企业内部文档或专业领域的数据库,从而使LLM在这些特定领域内提供更专业的回答。

RAG的一些应用场景包括:


  1. 智能客服: 能够根据最新的产品信息、FAQ等知识库回答用户的问题。
  2. 问答系统: 能够从大量的文档或数据中检索并生成针对特定问题的答案。
  3. 内容创作: 能够基于最新的研究报告或新闻资讯生成文章或报告。

总而言之,RAG通过将LLM的生成能力与信息检索系统的知识获取能力相结合,使得AI能够生成更准确、更可靠、更具上下文相关性的文本。

模型微调 (Fine-tuning) 则是指在预训练好的大型语言模型的基础上,使用一个较小但更 específico 的数据集进行额外的训练,以使模型更好地适应特定的任务或领域。这个过程会调整模型内部的权重和参数,使其学习到目标任务的特定模式和知识。


模型微调相较于RAG,在难度和不利方面主要体现在以下几点:


难度:


  1. 数据准备和标注: 模型微调通常需要一个高质量、与目标任务高度相关的标注数据集。收集、清洗和标注这些数据可能非常耗时、昂贵且困难,尤其是在某些专业领域。RAG主要依赖于构建和维护一个可搜索的知识库,对数据的标注要求通常较低。
  2. 专业知识: 微调需要对自然语言处理(NLP)、深度学习以及模型架构有一定的理解,才能选择合适的微调策略、调整超参数并评估模型性能。RAG的实现则更侧重于信息检索系统的构建和与LLM的集成。
  3. 实验和调优: 微调过程可能需要多次实验才能找到最佳的模型配置和超参数,以避免过拟合或欠拟合,并获得理想的性能。RAG的调优主要集中在检索策略和生成提示的优化上。
  4. 计算资源: 对大型LLM进行微调需要大量的计算资源(GPU/TPU)和时间,尤其是在处理大型数据集时。RAG在推理阶段的计算成本主要在于检索和生成答案,通常低于重新训练整个模型。

不利的地方:


  1. 知识更新困难: 微调后的模型所学习的知识是基于微调数据集的静态快照。如果需要模型掌握新的信息,必须重新进行微调,这既耗时又耗费资源。RAG可以通过更新外部知识库来轻松地引入和利用最新的信息。
  2. 泛化能力下降风险: 如果微调数据集过小或不够多样化,微调后的模型可能会过拟合于特定任务,导致在未见过的数据上的泛化能力下降。RAG通过利用外部知识库,模型仍然可以依赖其预训练的通用知识。
  3. 透明度和可解释性较低: 微调改变了模型内部的权重和参数,使得模型学习到的知识和决策过程更加难以解释和追溯。RAG在生成答案时可以提供检索到的外部证据,提高了透明度和可信度。
  4. 模型偏移 (Model Drift): 随着时间的推移,如果目标任务或数据的分布发生变化,微调后的模型性能可能会逐渐下降,需要定期维护和重新微调。RAG可以通过更新知识库来适应变化,但检索系统的有效性也可能受到数据质量的影响。
  5. 难以处理开放领域或不断演变的信息: 由于微调依赖于静态数据集,它不太适合需要处理不断变化或涉及广泛领域知识的任务。RAG通过实时检索外部信息,更适合这类场景。
特征RAG (检索增强生成)模型微调 (Fine-tuning)
核心思想实时检索外部知识并融入生成过程,增强模型对新知识的利用通过额外的训练调整模型内部参数,使其适应特定任务或领域
知识来源外部知识库 (实时或近实时)微调数据集 (静态)
更新知识相对容易,更新外部知识库即可需要重新进行微调,成本较高
适用场景需要处理最新信息、知识密集型、特定领域但知识 постоянно 更新的场景需要模型学习特定风格、术语、或执行特定任务,且知识相对稳定的场景
数据需求构建高质量的外部知识库和高效的检索系统需要高质量、标注良好的微调数据集 (通常比预训练数据小)
计算成本检索过程有计算成本,但通常低于重新训练整个模型微调过程需要一定的计算资源,尤其对于大型模型
灵活性较高,可以灵活更换和扩展知识库一旦微调完成,模型的知识和能力就相对固定
透明度可以提供检索到的证据,提高回答的可信度模型内部知识难以追溯,透明度较低
减少“幻觉”通过引用外部知识,有助于减少模型生成不实信息可以通过高质量的微调数据减少“幻觉”,但并非完全消除
实现难度需要构建信息检索系统和融合机制需要准备微调数据、选择合适的微调策略和超参数

总结来说:


  1. 选择 RAG 的情况: 当你需要模型能够回答关于最新信息、特定领域知识(这些知识可能会频繁更新)的问题,并且希望答案有外部证据支持时。RAG不需要改变模型本身,而是通过外部信息来增强其能力。
  2. 选择模型微调的情况: 当你需要模型学习特定的语言风格、理解特定领域的术语、或者在某个特定任务上获得更好的性能,并且你有相关的标注数据可以用来训练模型时。微调会改变模型内部的知识和行为方式。

在实践中,RAG和模型微调有时也可以结合使用,以充分发挥各自的优势。例如,可以先对模型进行微调,使其具备更好的领域理解能力,然后再使用RAG来增强其获取最新信息和生成更准确答案的能力。

GGUF格式和LLM量化类型

LLM GGUF 格式是一种用于存储大型语言模型(LLM)的文件格式,特别是那些与 Llama.cpp 库一起使用的模型。GGUF 是 “GG Ultra Fast” 的缩写,它旨在提供一种高效且可移植的方式来存储和加载 LLM。

GGUF 格式的关键特性包括:

  • 高效性: GGUF 格式的设计考虑了性能,允许 Llama.cpp 快速加载和处理模型数据。

  • 可移植性: GGUF 文件可以在不同的硬件和操作系统之间共享,从而提高了 LLM 的可用性。

  • 灵活性: GGUF 格式支持各种模型架构和数据类型,使其能够适应不断发展的 LLM 领域。

  • 向后兼容性: GGUF 格式设计为向后兼容,这意味着新版本的 Llama.cpp 仍然可以读取旧版本的 GGUF 文件。

总而言之,GGUF 格式是一种专门为 LLM 设计的存储格式,它强调效率、可移植性和灵活性。它在 Llama.cpp 生态系统中被广泛使用,并帮助实现了各种设备上 LLM 的高效部署。

Hugging Face 平台为使用 Llama.cpp 进行模型转换、量化和托管提供了各种在线工具:

GGUF-my-repo space:用于转换为 GGUF 格式,并将模型权重量化为更小的尺寸。

GGUF-my-LoRA space:用于将 LoRA 适配器转换为 GGUF 格式。

GGUF-editor space:用于在浏览器中编辑 GGUF 元数据。

Inference Endpoints:用于在云中直接托管 Llama.cpp 。

在 LLM 的 GGUF 格式中,Quantization 参数用于指定模型的量化方式。量化是一种模型压缩技术,通过降低模型参数的精度,来减少模型的大小和内存占用,同时提高推理速度。以下是 Quantization 参数中各种取值的含义:

  • q2_K:

    • 2 位量化,是最小的量化方式,可以实现最小的模型大小和最快的推理速度。
    • 但精度损失最大,适用于对精度要求不高的场景。
  • q3_K_M:

    • 3 位量化,精度比 q2_K 稍高,速度稍慢。
    • 适用于对精度有一定要求的场景。
  • q4_0:

    • 4 位量化,是常用的量化方式,可以在模型大小、速度和精度之间取得较好的平衡。
    • 适用于大多数场景。
  • q4_K_M:

    • 4 位量化,精度比 q4_0 稍高,速度稍慢。
    • 适用于对精度有较高要求的场景。
  • q5_0:

    • 5 位量化,精度比 q4 系列更高,速度稍慢。
    • 适用于需要更高精度的场景。
  • q5_K_M:

    • 5 位量化,精度比 q5_0 更高,速度稍慢。
    • 适用于对精度有更高要求的场景。
  • q6_K:

    • 6 位量化,精度较高,速度较慢。
    • 适用于对精度要求很高的场景。
  • q8_0: - 8 位量化,精度最高,速度最慢。- 适用于对精度要求极高的场景。

总结:

  • q 表示量化,后面的数字表示量化的位数。
  • K 和 M 表示不同的量化方法,它们在精度和速度之间有所权衡。
  • 量化位数越低,模型越小,速度越快,但精度越低。
  • 量化位数越高,模型越大,速度越慢,但精度越高。

Llama.cpp vs Ollama

Llama.cppOllama 都是在本地运行大型语言模型(LLMs)的工具,但它们的设计目标和使用方式有所不同。以下是它们之间的主要区别:

  1. 设计目标:
  • Llama.cpp:
    • Llama.cpp 是一个用 C++ 编写的库,专注于在消费级硬件(尤其是 CPU)上实现高性能的 LLM 推理。
    • 它的目标是提供一个轻量级、高效的解决方案,允许开发者在本地运行 LLMs,而无需依赖强大的 GPU。
    • Llama.cpp 的核心是提供一个高效的运行LLM的工具,可以理解为是一个底层的工具。
  • Ollama:
    • Ollama 则是一个更高级别的工具,旨在简化在本地运行 LLMs 的过程。
    • 它使用 Llama.cpp 作为其后端之一,提供了一个易于使用的命令行界面和 API,允许用户快速下载、运行和管理 LLMs,而无需深入了解底层细节。
    • Ollama 的核心是提供一个简易的LLM运行,下载,管理的工具,可以理解为是一个上层的工具。
  1. 使用方式:
  • Llama.cpp:
    • Llama.cpp 主要是一个库,开发者可以将其集成到自己的应用程序中。
    • 它也提供了一些命令行工具,但主要用于测试和演示。
    • Llama.cpp 需要用户对命令行和模型文件有一定的了解。
  • Ollama:
    • Ollama 提供了一个更友好的用户体验,用户可以通过简单的命令下载和运行 LLMs。
    • 它还提供了一个 API,允许开发者将 LLMs 集成到自己的应用程序中。
    • Ollama 简化了 LLMs 的安装和运行过程,降低了使用门槛。
  1. 功能:
  • Llama.cpp:
    • Llama.cpp 专注于提供高性能的 LLM 推理,支持多种量化和优化技术。
    • 它对底层硬件进行了优化,以实现最佳性能。
  • Ollama:
    • Ollama 除了提供 LLM 推理功能外,还提供了模型管理、API 服务等功能。
    • 它旨在提供一个完整的 LLM 运行环境。
  1. 总结:
  • 如果您是开发者,需要将 LLMs 集成到自己的应用程序中,并且对性能有较高要求,那么 Llama.cpp 可能更适合您。
  • 如果您是普通用户,希望快速、方便地在本地运行 LLMs,那么 Ollama 可能更适合您。
  • 简单来说,Llama.cpp 是一个更底层的工具,而 Ollama 则是一个更上层的工具。

GraphRAG vs AnythingLLM

  • 核心理念: GraphRAG 是一种检索增强生成(RAG)技术,它利用知识图谱来增强大型语言模型(LLM)的性能。

  • 工作原理:

    • 它从知识图谱中检索相关信息,并将其作为上下文提供给 LLM。这有助于 LLM 更准确、更全面地回答问题,尤其是在需要复杂推理或涉及实体关系时。
  • 优势:

    • 增强了 LLM 的事实准确性和知识覆盖面。
    • 提高了处理复杂查询和推理的能力。
    • 能够更好地理解实体之间的关系。
  • 应用场景:

    • 金融分析、医疗保健、法律等需要精确知识和推理的领域。
    • 在处理具有复杂关系的数据时,例如社交网络分析。
  • 特点:

    • 通过知识图谱来提高数据的关系性和准确性。
    • 需要构建和维护知识图谱。
  • 核心理念: Anything LLM 是一种开源平台,旨在简化 LLM 与各种数据源的集成。

  • 工作原理:

    • 它允许用户连接各种数据源(如文档、网站、数据库等)。
    • 然后,它使用 RAG 技术从这些数据源中检索相关信息,并将其提供给 LLM。
  • 优势:

    • 易于使用,无需编码即可连接数据源。
    • 支持多种数据源,具有很强的灵活性。
    • 开源,允许用户自定义和扩展。
  • 应用场景:

    • 构建问答系统、聊天机器人、文档摘要等。
    • 在需要从多个数据源检索信息的场景中。
  • 特点:

    • 可以连接多种数据源。
    • 拥有友好的UI界面,容易上手。
    • 开源项目,具有活跃的社区。
  • 主要区别

    • GraphRAG 的重点是利用知识图谱,而 Anything LLM 的重点是简化数据源集成。
    • GraphRAG更侧重于提高LLM的推理能力,而AnythingLLM更侧重于提高LLM的信息检索能力。
    • GraphRAG通常需要构建知识图谱,而AnythingLLM则更加的通用,可以连接多种数据源。
  • 总结

    • 如果需要利用知识图谱来提高 LLM 的推理能力,GraphRAG 可能是一个不错的选择。
    • 如果需要一个易于使用的平台来连接各种数据源,Anything LLM 可能更适合。

简单对比GraphRAG和LlamaIndex

比较LlamaIndex和GraphRAG的成熟度,需要考虑它们各自的目标、功能以及实际应用情况。以下是一些关键点的比较:

  • 成熟度:
    • LlamaIndex是一个为了简化LLM(大型语言模型)应用中的数据增强检索(RAG)流程的工具。
    • 它专注于提供各种数据连接器、索引和查询工具,以便LLM能够更好地利用外部数据。
    • LlamaIndex在RAG领域中相对成熟,拥有活跃的社区和广泛的文档。
    • 它提供了多种索引类型,支持多种数据源,并且易于使用。
  • 功能:
    • 数据连接器:支持多种数据源,如PDF、网站、数据库等。
    • 数据索引:提供向量索引、树索引、关键词索引等多种索引方式。
    • 查询引擎:提供各种查询方式,支持高级检索。
    • 生态丰富:拥有大量的社区贡献的工具和插件。
  • 应用:
    • 广泛应用于构建问答系统、聊天机器人、知识库等LLM应用。
    • 适用于需要利用外部数据增强LLM能力的场景。
  • 成熟度:
    • GraphRAG是一种新兴的技术,它结合了知识图谱和RAG,旨在提高LLM在处理复杂信息时的准确性和相关性。
    • 虽然GraphRAG的原理已经相对成熟,但其在实际应用中的稳定性和成熟度还处于发展中。
    • GraphRAG的应用案例相对较少,主要集中在一些研究项目和探索性应用中。
  • 功能:
    • 知识图谱构建:将非结构化文本转化为结构化的知识图谱。
    • 图引导检索:利用知识图谱的结构信息,提高检索的准确性和效率。
    • 增强生成:利用知识图谱的结构信息,验证和修正LLM生成的文本。
  • 应用:
    • 适用于需要处理复杂关系和实体信息的场景,如金融、医疗、法律等。
    • 在需要推理和多跳问答的场景中具有潜力。
  • LlamaIndex在RAG领域中相对成熟,拥有更广泛的应用和更丰富的生态。
  • GraphRAG是一种新兴的技术,具有很大的发展潜力,但其成熟度和稳定性还处于发展中。
  • 从一些测试结果上看,GraphRAG在对全局的理解上,确实强大,但是从时间,成本上看,消耗也很大,而LlamaIndex在时间和成本上,占有优势。
  • GraphRAG的回答质量不是在所有场景适用,而是依赖于高质量的知识图谱。

如果需要一个成熟、稳定且易于使用的RAG工具,LlamaIndex是一个不错的选择。 如果您需要处理复杂的知识密集型任务,并且愿意投入更多的时间和资源,GraphRAG可能是一个值得探索的方向。

Embedding

简单来说,Embedding 模型是一种将高维度、离散或复杂的输入数据(例如文字、图片、用户ID、商品ID等)转换为低维度、连续的向量(Vector)表示的技术或模型。这个生成的向量被称为“嵌入”(Embedding)。

想象一下,我们有很多词语,比如“国王”、“女王”、“男人”、“女人”。直接用这些词语本身,计算机很难理解它们之间的关系。Embedding 模型就能学习到将这些词语映射到一个多维空间中的点(向量)。在这个空间里,“国王”和“男人”的距离会比较近,“女王”和“女人”的距离会比较近,而“国王”和“女王”之间可能存在一种类似于“男人”到“女人”的关系向量。

  1. 降维: 将原本可能维度非常高(比如用 one-hot 编码表示词语,维度可能高达几万甚至几十万)或者非结构化的数据,映射到一个维度相对较低(通常是几十到几百维)的连续向量空间。

  2. 保留语义/关系: 这个映射过程不是随机的,而是通过学习大量数据得到的。目标是让转换后的向量能够捕捉到原始数据中的内在含义、相似性或关系。在向量空间中,语义相近或关系类似的对象,它们的向量也会比较接近或具有特定关联。

  3. 利于计算: 计算机更擅长处理数值型的向量。将各种类型的数据转换为统一的向量表示后,就可以方便地进行各种数学运算,如计算相似度(点积、余弦相似度)、距离(欧氏距离)等,进而应用于各种下游任务。

Embedding 模型的能力非常广泛,是许多现代机器学习和人工智能应用的基础模块。主要作用包括:

  1. 语义理解与表示 (Semantic Understanding & Representation):

    • 自然语言处理 (NLP): 这是 Embedding 最经典的应用领域。

      • 词嵌入 (Word Embeddings):Word2Vec, GloVe, FastText,将单词映射为向量,捕捉词语的语义和语法关系。

      • 句子/文档嵌入 (Sentence/Document Embeddings):Sentence-BERT, Universal Sentence Encoder,将整个句子或文档表示为向量,用于文本分类、情感分析、问答系统、文本相似度计算等。

    • 知识图谱 (Knowledge Graphs): 将实体(如人物、地点)和关系(如“出生在”、“工作于”)嵌入到向量空间,用于知识推理和链接预测。

  2. 相似性计算与搜索 (Similarity Calculation & Search):

    • 信息检索/语义搜索: 通过比较查询(Query)的 Embedding 和数据库中文档/物品的 Embedding,找到语义最相关的结果,而不是仅仅基于关键词匹配。例如,搜索“夏天穿的透气鞋子”,能找到包含“凉鞋”、“网面运动鞋”等词语的商品,即使查询中没有这些具体词。

    • 图像/音频检索: 将图像或音频转换为 Embedding,实现以图搜图、以歌搜歌等功能。

  3. 推荐系统 (Recommendation Systems):

    • 协同过滤: 将用户(User)和物品(Item)都嵌入到同一个向量空间。可以通过计算用户 Embedding 和物品 Embedding 的相似度来预测用户可能喜欢的物品,或者找到具有相似兴趣的用户(计算用户 Embedding 之间的相似度)。

    • 内容推荐: 基于物品内容的 Embedding(如文章内容、商品描述)来推荐相似的物品。

  4. 分类与聚类 (Classification & Clustering):

    • 将原始数据转换为 Embedding 后,这些向量可以作为特征输入到传统的分类器(如 SVM、逻辑回归)或聚类算法(如 K-Means)中,通常能提高模型的性能,因为 Embedding 包含了更丰富的语义信息。
  5. 异常检测 (Anomaly Detection):

    • 正常的数据点在 Embedding 空间中可能会聚集在一起,而异常点则可能远离这些聚集区,从而可以被识别出来。
  6. 数据可视化 (Data Visualization):

    • 虽然 Embedding 本身是高维的,但可以使用降维技术(如 t-SNE, PCA)将其投影到二维或三维空间进行可视化,帮助我们直观地理解数据点之间的关系和结构。

总结来说,Embedding 模型的核心价值在于:

Section titled “总结来说,Embedding 模型的核心价值在于:”
  • 将复杂数据转化为计算机易于处理的数值向量。

  • 在转化过程中捕捉并保留数据的内在语义和关系。

  • 作为许多高级 AI 应用(如搜索、推荐、NLP)的基础,提升其效果和智能程度。

它就像是为不同类型的数据(文本、图像、用户行为等)构建了一个通用的“语义坐标系”,使得我们可以在这个统一的空间中进行有意义的比较、查找和分析。