這是一篇關於個人學習 AI 的筆記與程式碼摘錄。希望從前端的視角出發,快速了解大型語言模型(LLM)、提示詞工程、LangChain、RAG 等相關術語知識,最終能夠搭建一個 “玩具智能體” 或者真正應用到生產中去。
要理解 AI 應用,我們首先要抓住大型語言模型(LLM)的核心。它的本質其實非常樸素:一次一個 token 地補全內容,或者說,不斷地“預測下一個詞”。
比如下面,會計算接下來字符的出現概率
透過不斷預測下個詞,最終生成一段話:
但這個預測過程並非總是選擇概率最高的詞,否則每次的回答都會一成不變。為了引入創造性,模型在採樣時會加入一些隨機性。這可以透過 temperature
(溫度) 這個參數來控制:
temperature
(如 0.2): 回答更具確定性和穩定性,適合需要事實性回答的場景。temperature
(如 0.8): 回答更具發散性和創意。那麼,模型是如何理解我們輸入的文字的呢?計算機會將文字(字串)轉換為數字(向量)。這個過程通常分為兩步:第一步 One-Hot 編碼,第二步,將編碼結果進行壓縮。
如下圖,文本中的每個 Token 都會對應一個向量。起初,這是一個包含大量 0 的稀疏向量。
經過 Embedding 處理後,一個碩大的向量就會壓縮成一個固定大小的向量。比如在 GPT3 中,每個 Token 由768個數字組成的向量表示。
在與大模型 API(以 OpenAI 為例)互動時,有一些常用參數可以幫助我們精確地控制模型的行為。這裡我們對它們進行一個簡單的記錄和解釋,以便理解和查詢(不同的大模型 API 參數會有差異)。
這些是每次調用時幾乎都會用到的基礎參數。
用於擴展模型的能力,讓它能與外部世界互動。
用於更精細地調整模型的生成策略。
temperature
的核採樣方法,控制生成文本的多樣性。與大模型溝通的藝術,就是提示詞工程(Prompt Engineering)。一個好的提示詞能極大提升模型的表現。
從用戶的角度來看,一個結構化的提示詞可以遵循這樣一個公式:
提示詞 = 定義角色 + 背景信息 + 任務目標 + 輸出要求
在實踐中,有幾種常見的提示詞範式可以幫助我們更好地引導模型:
LangChain 是一個強大的開源框架,它能幫助我們輕鬆地構建和組合各種大模型應用,我們稱之為“鏈”(Chain)。
它的生態還包括:
其核心是 LCEL(LangChain 表達式語言),一種用管道符 |
將不同組件聲明式地組裝在一起的表達式語言,非常清晰且易於重用。
【【---下面程式碼摘自 python 學習資料,不需要完全理解具體實現,能知道有那麼個 API 能夠在各階段進行相應的處理就行了。---】】
從最基礎的開始:如何與一個聊天模型進行互動。下面的程式碼展示了如何發送系統和用戶消息,並獲得模型的回覆。
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
import os
os.environ["OPENAI_API_KEY"] = "你的 API Key"
os.environ["OPENAI_API_BASE"] = "你的 API Base"
messages = [
SystemMessage(content="Translate the following from English into Chinese:"),
HumanMessage(content="Welcome to LLM application development!"),
]
model = ChatOpenAI(model="gpt-4o-mini")
result = model.invoke(messages)
print(result)
為了讓程式碼更清晰、更易於維護,我們通常不會把給開發者的指令和用戶的輸入混在一起。LangChain 提供了 PromptTemplate
來優雅地解決這個問題。
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt_template = ChatPromptTemplate.from_messages(
[
("system", "Translate the following from English into Chinese:"),
("user", "{text}"),
]
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt_template | model
result = chain.invoke({"text": "Welcome to LLM application development!"})
print(result)
有時我們希望模型返回的是嚴格的 JSON 格式或其他結構化數據,而不是純文本。OutputParser
可以幫助我們定義輸出格式,並自動解析模型的返回結果。
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class Work(BaseModel):
title: str = Field(description="Title of the work")
description: str = Field(description="Description of the work")
parser = JsonOutputParser(pydantic_object=Work)
prompt = PromptTemplate(
template="列舉3部{author}的作品。\n{format_instructions}",
input_variables=["author"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | parser
result = chain.invoke({"author": "老舍"})
print(result)
標準的 API 調用是無狀態的,但聊天機器人需要記住之前的對話。LangChain 提供了多種方式來管理會話歷史,讓我們的應用能夠進行連續的多輪對話。
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
chat_model = ChatOpenAI(model="gpt-4o-mini")
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
with_message_history = RunnableWithMessageHistory(chat_model, get_session_history)
config = {"configurable": {"session_id": "dreamhead"}}
while True:
user_input = input("You:> ")
if user_input.lower() == 'exit':
break
stream = with_message_history.stream(
[HumanMessage(content=user_input)],
config=config
)
for chunk in stream:
print(chunk.content, end='', flush=True)
print()
大型模型雖然知識淵博,但它的知識是靜態的(截止到某個訓練日期),而且不包含你的私有數據。
那麼,如何讓模型“知道更多”呢?答案就是 RAG(Retrieval-Augmented Generation),即檢索增強生成。
它的核心思想很簡單,就是“先查後答”。具體流程如下:
用戶問題
↓
文本轉 Embedding → 檢索知識庫(向量匹配)
↓
找到相關內容
↓
大模型生成答案(融合外部信息)
關於向量數據庫的見解
向量數據庫的核心,是將文本等多模態數據轉化為高維空間中的向量。每一個詞、每一段話,都相應地成為 N 維空間中的一個點。
這種表示方式的強大之處在於,我們可以透過計算這些點之間的“距離”或“夾角”來量化它們的語義相似度。以二維空間為例,兩個向量的點積可以揭示它們的關係:
- 銳角 (點積為正): 表示語義相似。
- 垂直 (點積為零): 表示語義無關。
- 鈍角 (點積為負): 表示語義背離或相反。
這種從幾何角度理解語義的算法,不僅是 RAG 的基石,也是許多推薦系統的核心原理,讓機器能夠在海量信息中找到“鄰近”的內容。
首先,我們需要將文檔加載、切分、並轉換為向量,存入專門的向量數據庫中。
from langchain_community.document_loaders import TextLoader
loader = TextLoader("introduction.txt")
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma(
collection_name="ai_learning",
embedding_function=OpenAIEmbeddings(),
persist_directory="vectordb"
)
vectorstore.add_documents(splits)
當用戶提問時,我們從向量數據庫中檢索最相似的文檔片段,並將其提供給模型。
vectorstore = Chroma(
collection_name="ai_learning",
embedding_function=OpenAIEmbeddings(),
persist_directory="vectordb"
)
retriever = vectorstore.as_retriever(search_type="similarity")
# Retriever 承擔 RAG 中的 R:根據文本查詢文檔(Document)
關鍵點:RAG 的成功與否,很大程度上取決於數據處理的質量。包括:
- 如何有效地提取數據源
- 选择合适的分塊(Chunking)策略
- 優化向量化和檢索算法等
現代的 AI 應用早已超越了純文本的範疇。
多模態意味著模型能夠理解和處理多種類型的信息,如文本、圖片、音頻和視頻。作為前端開發者,我們需要掌握如何在客戶端處理這些複雜的輸入和輸出。
下面,我們將透過幾個具體的場景,來看看前端是如何與多模態模型進行互動的。
這是最基礎的交互。除了“一次性”返回所有結果,更優的用戶體驗是“流式”返回,即像打字機一樣逐字顯示內容。
在前端,這通常透過 fetch
API 結合 ReadableStream
來實現。
Chat.vue
):// ...
if (stream.value) {
const completion = await openai.chat.completions.create({
model: MODEL,
stream: true, // 關鍵參數
messages: [ /* ... */ ]
});
// 逐塊讀取流
for await (const chunk of completion as any) {
const delta = chunk?.choices?.[0]?.delta?.content;
if (delta) content.value += delta;
}
} else {
const completion = await openai.chat.completions.create({ /* ... */ });
content.value = completion.choices?.[0]?.message?.content || '(無返回)';
}
// ...
stream: true
時,返回的是一個數據流。我們透過 for await...of
循環來異步地迭代這個流,每次迭代得到一個數據塊(chunk),然後將增量內容(delta)追加到界面上,實現了流暢的打字機效果。調用文生圖模型時,前端需要構造一個包含詳細參數的請求,並將返回的圖片 URL 展示出來。
image.vue
):// ...
const body = {
model: 'qwen-image',
input: {
messages: [
{ role: 'user', content: [ { text: prompt.value.trim() } ] }
]
},
parameters: {
negative_prompt: negativePrompt.value || '',
size: size.value
}
};
const resp = await fetch('/api/.../multimodal-generation/generation', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify(body)
});
const json = await resp.json();
// 從複雜的 JSON 結構中解析出圖片 URL
const choices = json?.output?.choices || [];
for (const c of choices) {
const contents = c?.message?.content || [];
if (Array.isArray(contents)) {
for (const item of contents) {
if (item?.image && typeof item.image === 'string') list.push(item.image);
}
}
}
// ...
<img>
標籤上。為了實現低延遲的語音合成,前端可以接收實時的音頻流並立即播放,而不是等待整個音頻文件生成完畢。這通常使用 SSE (Server-Sent Events) 協議。
TTS.vue
):// 1. 發起 SSE 請求
const resp = await fetch('/api/.../multimodal-generation/generation', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-DashScope-SSE': 'enable' // 啟用 SSE
},
body: JSON.stringify(body),
});
// 2. 實時處理音頻流
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// ... 解析 SSE 訊息 ...
const chunkB64 = out.audio_chunk; // 獲取 Base64 編碼的 PCM 音頻塊
if (!chunkB64) continue;
const pcm = base64ToBytes(chunkB64);
playPcmRealtime(pcm, sampleRate); // 實時播放
}
// 3. 使用 Web Audio API 播放 PCM 數據
let audioCtx = new AudioContext();
let scheduledTime = 0;
function playPcmRealtime(pcm, sr) {
if (!audioCtx) return;
const frameCount = pcm.length / 2; // 16-bit
const abuf = audioCtx.createBuffer(1, frameCount, sr);
// ... 將 PCM 數據寫入 AudioBuffer ...
const src = audioCtx.createBufferSource();
src.buffer = abuf;
src.connect(audioCtx.destination);
src.start(scheduledTime); // 精確調度播放時間,避免爆音
scheduledTime += abuf.duration;
}
// ...
'X-DashScope-SSE': 'enable'
來告訴服務端我們需要一個 SSE 連接。ReadableStream
和 TextDecoder
來逐行讀取和解析服務端推送的事件。Web Audio API
(AudioContext
) 將這些 PCM 數據解碼成 AudioBuffer
,並透過 createBufferSource
創建一個音頻源進行無縫播放,實現了幾乎無延遲的語音合成效果。視頻生成通常是耗時很長的異步任務。前端在提交請求後不會立刻得到結果,而是會收到一個任務 ID。之後,前端需要透過這個 ID 定期去輪詢(Poll)任務狀態,直到任務完成並獲取視頻 URL。
Video.vue
):// 1. 提交異步任務
const resp = await fetch('/api/.../video-synthesis', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-DashScope-Async': 'enable' // 啟用異步模式
},
body: JSON.stringify(body)
});
const json = await resp.json();
const returnedTaskId = json?.output?.task_id;
if (returnedTaskId) {
pollTask(returnedTaskId); // 開始輪詢
}
// 2. 轮询任务状态
const pollTask = async (id, interval = 5000) => {
const endpoint = `/api/v1/tasks/${id}`;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const r = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${apiKey}` } });
const j = await r.json();
const s = j?.output?.task_status;
const v = findVideoUrl(j); // 嘗試從返回中尋找視頻 URL
if (v) {
videoUrl.value = v; // 找到了!停止輪詢
return;
}
if (s === 'SUCCEEDED') { /* 任務成功但可能 URL 在別處,繼續解析 */ }
await new Promise(res => setTimeout(res, interval)); // 等待 5 秒再查
}
};
'X-DashScope-Async': 'enable'
來啟動一個異步任務。task_id
。pollTask
函數,該函數會每隔幾秒鐘(例如 5 秒)調用任務查詢接口,檢查任務狀態。SUCCEEDED
或直接在響應中找到視頻 URL 時,輪詢結束,前端將視頻展示給用戶。對於 OCR 這樣的視覺語言模型,API 調用方式也發生了變化。我們需要在一個請求中同時包含圖片和文本指令。
Ocr.vue
):// ...
const body = {
model: 'qwen-vl-ocr',
messages: [
{ role: 'user', content: [
{ type: 'image_url', image_url: imageUrl.value.trim() },
{ type: 'text', text: buildPrompt() } // "請識別圖片中全部文字..."
]}
]
};
const resp = await fetch('.../chat/completions', { /* ... */ });
// ...
messages
陣列中,content
不再是一個簡單的字串,而是一個包含不同類型物件的陣列。
{ type: 'image_url', ... }
用來指定圖片地址。{ type: 'text', ... }
用來給出具體的指令(比如要求返回純文本還是 JSON)。在構建更複雜的 AI 應用時,我們會接觸到一些關鍵的工具和協議:
AI 的世界這麼大,後面會邊學邊補充,覺得有幫助不妨點個收藏夾~~