RAG應(yīng)用在得物開放平臺的智能答疑的探索
一、背景
得物開放平臺是一個把得物能力進(jìn)行開放,同時提供給開發(fā)者提供 公告、應(yīng)用控制臺、權(quán)限包申請、業(yè)務(wù)文檔等功能的平臺。
- 面向商家:通過接入商家自研系統(tǒng)??梢詫?shí)現(xiàn)自動化庫存、訂單、對賬等管理。
- 面向ISV :接入得物開放平臺,能為其產(chǎn)品提供更完善的全平臺支持。
- 面向內(nèi)部應(yīng)用:提供安全、可控的、快速支持的跨主體通訊。
得物開放平臺目前提供了一系列的文檔以及工具去輔助開發(fā)者在實(shí)際調(diào)用API之前進(jìn)行基礎(chǔ)的引導(dǎo)和查詢。
但目前的文檔搜索功能僅可以按照接口路徑,接口名稱去搜索,至于涉及到實(shí)際開發(fā)中遇到的接口前置檢查,部分字段描述不清等實(shí)際問題,且由于信息的離散性,用戶想要獲得一個問題的答案需要在多個頁面來回檢索,造成用戶焦慮,進(jìn)而增大TS的答疑可能性。
隨著這幾年AI大模型的發(fā)展,針對離散信息進(jìn)行聚合分析且精準(zhǔn)回答的能力變成了可能。而RAG應(yīng)用的出現(xiàn),解決了基礎(chǔ)問答類AI應(yīng)用容易產(chǎn)生幻覺現(xiàn)象的問題,達(dá)到了可以解決實(shí)際應(yīng)用內(nèi)問題的目標(biāo)。
二、簡介
什么是RAG
RAG(檢索增強(qiáng)生成)指Retrieval Augmented Generation。
這是一種通過從外部來源獲取知識來提高生成性人工智能模型準(zhǔn)確性和可靠性的技術(shù)。通過RAG,用戶實(shí)際上可以與任何數(shù)據(jù)存儲庫進(jìn)行對話,這種對話可視為“開卷考試”,即讓大模型在回答問題之前先檢索相關(guān)信息。
RAG應(yīng)用的可落地場景
RAG應(yīng)用的根本是依賴一份可靠的外部數(shù)據(jù),根據(jù)提問檢索并交給大模型回答,任何基于可靠外部數(shù)據(jù)的場景均是RAG的發(fā)力點(diǎn)。
RAG應(yīng)用的主要組成部分
- 外部知識庫:問題對應(yīng)的相關(guān)領(lǐng)域知識,該知識庫的質(zhì)量將直接影響最終回答的效果。
- Embedding模型:用于將外部文檔和用戶的提問轉(zhuǎn)換成Embedding向量。
- 向量數(shù)據(jù)庫:將外部信息轉(zhuǎn)化為Embedding向量后進(jìn)行存儲。
- 檢索器:該組件負(fù)責(zé)從向量數(shù)據(jù)庫中識別最相關(guān)的信息。檢索器將用戶問題轉(zhuǎn)換為Embedding向量后執(zhí)行相似性檢索,以找到與用戶查詢相關(guān)的Top-K文檔(最相似的K個文檔)。
- 生成器(大語言模型LLM):一旦檢索到相關(guān)文檔,生成器將用戶查詢和檢索到的文檔結(jié)合起來,生成連貫且相關(guān)的響應(yīng)。
- 提示詞工程(Prompt Engineering):這項技術(shù)用于將用戶的問題與檢索到的上下文有效組合,形成大模型的輸入。
RAG應(yīng)用的核心流程
以下為一個標(biāo)準(zhǔn)RAG應(yīng)用的基礎(chǔ)流程:
- 將查詢轉(zhuǎn)換為向量
- 在文檔集合中進(jìn)行語義搜索
- 將檢索到的文檔傳遞給大語言模型生成答案
- 從生成的文本中提取最終答案
但在實(shí)際生產(chǎn)中,為了確保系統(tǒng)的全面性、準(zhǔn)確性以及處理效率,還有許多因素需要加以考慮和處理。
下面我將基于答疑助手在開放平臺的落地,具體介紹每個步驟的詳細(xì)流程。
三、實(shí)現(xiàn)目標(biāo)
鑒于目前得物開放平臺的人工答疑數(shù)量相對較高,用戶在開放平臺查詢未果就會直接進(jìn)入到人工答疑階段。正如上文所說,RAG擅長依賴一份可靠的知識庫作出相應(yīng)回答,構(gòu)建一個基于開放平臺文檔知識庫的RAG應(yīng)用再合適不過,同時可以一定程度降低用戶對于人工答疑的依賴性,做到問題前置解決。
四、整體流程
技術(shù)選型
- 大模型:https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/
- Embedding模型:https://platform.openai.com/docs/guides/embeddings
- 向量數(shù)據(jù)庫:https://milvus.io/
- 框架:https://js.langchain.com/v0.2/docs/introduction/LangChain.js是LangChain的JavaScript版本,專門用于開發(fā)LLM相關(guān)的交互應(yīng)用程序,其Runnable設(shè)計在開放平臺答疑助手中廣泛應(yīng)用,在拓展性、可移植性上相當(dāng)強(qiáng)大。
準(zhǔn)確性思考
問答的準(zhǔn)確性會直接反饋到用戶的使用體驗,當(dāng)一個問題的回答是不準(zhǔn)確的,會導(dǎo)致用戶根據(jù)不準(zhǔn)確的信息進(jìn)一步犯錯,導(dǎo)致人工客服介入,耐心喪失直至投訴。
所以在實(shí)際構(gòu)建基于開放平臺文檔的答疑助手之前,首先考慮到的是問答的準(zhǔn)確性,主要包括以下2點(diǎn):
- 首要解決答疑助手針對非開放平臺提問的屏蔽
- 尋找可能導(dǎo)致答非所問的時機(jī)以及相應(yīng)的解決方案
屏蔽非相關(guān)問題
為了屏蔽AI在回答時可能會回答一些非平臺相關(guān)問題,我們首先要做的是讓AI明確我們的目標(biāo)(即問答上下文),且告訴他什么樣的問題可以回答,什么問題不可以回答。
在這一點(diǎn)上,常用的手段為告知其什么是開放平臺以及其負(fù)責(zé)的范疇。
例如:得物的開放平臺是一個包含著 API 文檔,解決方案文檔的平臺,商家可以通過這個平臺獲取到得物的各種接口,以及解決方案,幫助商家更好的使用得物的服務(wù)?,F(xiàn)在需要做一個智能答疑助手,你是其中的一部分。
在這一段描述中,我們告知了答疑助手,開放平臺包含著API文檔,包含著解決方案,同時包含接口信息,同時會有商家等之類的字眼。大模型在收到這段上下文后,將會對其基礎(chǔ)回答進(jìn)行判斷。
同時,我們可以通過讓答疑助手二選一的方式進(jìn)行回答,即平臺相關(guān)問題與非平臺相關(guān)問題。我們可以讓大模型返回特定的數(shù)據(jù)枚舉,且限定枚舉范圍,例如:開放平臺通用問題、開放平臺API答疑問題,未知問題。
借助Json類型的輸出 + JSON Schema,我們可通過Prompt描述來限定其返回,從而在進(jìn)入實(shí)際問答前做到事前屏蔽。
尋找可能導(dǎo)致答非所問的時機(jī)
當(dāng)問題被收攏到開放平臺這個主題之后,剩余的部分就是將用戶提問與上下文進(jìn)行結(jié)合,再交由大模型回答處理。在這過程中,可能存在的答非所問的時機(jī)有:不夠明確的Prompt說明、上下文信息過于碎片化以及上下文信息的連接性不足三種。
- 不夠明確的Prompt說明:Prompt本身描述缺少限定條件,導(dǎo)致大模型回答輕易超出我們給予的要求,從而導(dǎo)致答非所問。
- 上下文信息過于碎片化:上下文信息可能被分割成N多份,這個N值過大或者過小,都會導(dǎo)致單個信息過大導(dǎo)致缺乏聯(lián)想性、單個信息過小導(dǎo)致回答時不夠聚焦。
- 上下文信息連接性不夠:若信息之間被隨意切割,且缺少相關(guān)元數(shù)據(jù)連接,交給大模型的上下文將會是喪失實(shí)際意義的文本片段,導(dǎo)致無法提取出有用信息,從而答非所問。
為了解決以上問題,在設(shè)計初期,開放平臺答疑助手設(shè)定了以下策略來前置解決準(zhǔn)確性問題:
- 用戶提問的結(jié)構(gòu)化
- 向量的分割界限以及元信息處理
- CO-STAR Prompt結(jié)構(gòu)
- 相似性搜索的K值探索
用戶提問結(jié)構(gòu)化
目標(biāo):通過大模型將用戶提問的結(jié)構(gòu)化,將用戶提問分類并提取出精確的內(nèi)容,便于提前引導(dǎo)、終止以及提取相關(guān)信息。
例如,用戶提問今天天氣怎么樣,結(jié)構(gòu)化Runnable會將用戶問題進(jìn)行初次判斷。
一個相對簡單的Prompt實(shí)現(xiàn)如下:
# CONTEXT
得物的開放平臺是一個包含著 API 文檔,解決方案文檔的平臺,商家可以通過這個平臺獲取到得物的各種接口,以及解決方案,幫助商家更好的使用得物的服務(wù)?,F(xiàn)在需要做一個智能答疑助手,你是其中的一部分。
# OBJECTIVE
你現(xiàn)在扮演一名客服。請將每個客戶問題分類到固定的類別中。
你只接受有關(guān)開放平臺接口的相關(guān)問答,不接受其余任何問題。
具體的類別我會在提供給你的JSON Schema中進(jìn)行說明。
# STYLE
你需要把你的回答以特定的 JSON 格式返回
# TONE
你給我的內(nèi)容里,只能包含特定 JSON 結(jié)構(gòu)的數(shù)據(jù),不可以返回給我任何額外的信息。
# AUDIENCE
你的回答是給機(jī)器看的,所以不需要考慮任何人類的感受。
# RESPONSE
你返回的數(shù)據(jù)結(jié)構(gòu)必須符合我提供的 JSON Schema 規(guī)范,我給你的 Schema 將會使用\`<json-schema></json-schema>\`標(biāo)簽包裹.
每個字段的描述,都是你推算出該字段值的依據(jù),請仔細(xì)閱讀。
<json-schema>
{schema}
</json-schema>
Json Schema的結(jié)構(gòu)通過zod描述如下:
const zApiCallMeta = z
.object({
type: z
.enum(['api_call', 'unknown', 'general'])
.describe('當(dāng)前問題的二級類目, api_call為API調(diào)用類問題,unknown為非開放平臺相關(guān)問題, general為通用類開放平臺問題'),
apiName: z
.string()
.describe(
'接口的名稱。接口名稱為中文,若用戶未給出明確的API中文名稱,不要隨意推測,將當(dāng)前字段置為空字符串',
),
apiUrl: z.string().describe('接口的具體路徑, 一般以/開頭'),
requestParam: z.unknown().default({}).describe('接口的請求參數(shù)'),
response: z
.object({})
.or(z.null())
.default({})
.describe('接口的返回值,若未提供則返回null'),
error: z
.object({
traceId: z.string(),
})
.optional()
.describe('接口調(diào)用的錯誤信息,若接口調(diào)用失敗,則提取traceId并返回'),
})
.describe('當(dāng)二級類目為api_call時,使用這個數(shù)據(jù)結(jié)構(gòu)');
以上結(jié)構(gòu),將會對用戶的問題輸入進(jìn)行結(jié)構(gòu)化解析。同時給出相應(yīng)JSON數(shù)據(jù)結(jié)構(gòu)。
將以上結(jié)構(gòu)化信息結(jié)合,可實(shí)現(xiàn)一個基于LangChain.js的結(jié)構(gòu)化Runnable,在代碼結(jié)構(gòu)設(shè)計上,所有的Runnable將會使用$作為變量前綴,用于區(qū)分Runnable與普通函數(shù)。
import { ChatOpenAI } from '@langchain/openai';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
import { $getPrompt } from './$prompt';
import { zSchema, StructuredInputType } from './schema';
import { n } from 'src/utils/llm/gen-runnable-name';
import { getLLMConfig } from 'src/utils/llm/get-llm-config';
import { getStringifiedJsonSchema } from 'src/utils/llm/get-stringified-json-schema';
const b = n('$structured-input');
const $getStructuredInput = () => {
const $model = new ChatOpenAI(getLLMConfig().ChatOpenAIConfig).bind({
response_format: {
type: 'json_object',
},
});
const $input = RunnableMap.from<{ question: string }>({
schema: () => getStringifiedJsonSchema(zSchema),
question: (input) => input.question,
}).bind({ runName: b('map') });
const $prompt = $getPrompt();
const $parser = new StringOutputParser();
return RunnableSequence.from<{ question: string }, string>([
$input.bind({ runName: b('map') }),
$prompt.bind({ runName: b('prompt') }),
$model,
$parser.bind({ runName: b('parser') }),
]).bind({
runName: b('chain'),
});
};
export { $getStructuredInput, type StructuredInputType };
鑒于CO-STAR以及JSONSchema的提供的解析穩(wěn)定性,此Runnable甚至具備了可單測的能力。
import dotenv from 'dotenv';
dotenv.config();
import { describe, expect, it } from 'vitest';
import { zSchema } from '../runnables/$structured-input/schema';
import { $getStructuredInput } from '../runnables/$structured-input';
const call = async (question: string) => {
return zSchema.safeParse(
JSON.parse(await $getStructuredInput().invoke({ question })),
);
};
describe('The LLM should accept user input as string, and output as structured data', () => {
it('should return correct type', { timeout: 10 * 10000 }, async () => {
const r1 = await call('今天天氣怎么樣');
expect(r1.data?.type).toBe('unknown');
const r2 = await call('1 + 1');
expect(r2.data?.type).toBe('unknown');
const r3 = await call('trace: 1231231231231231313');
expect(r3.data?.type).toBe('api_call');
const r4 = await call('快遞面單提示錯誤');
expect(r4.data?.type).toBe('api_call');
const r5 = await call('發(fā)貨接口是哪個');
expect(r5.data?.type).toBe('api_call');
const r6 = await call('怎么發(fā)貨');
expect(r6.data?.type).toBe('general');
const r7 = await call('獲取商品詳情');
expect(r7.data?.type).toBe('api_call');
const r8 = await call('dop/api/v1/invoice/cancel_pick_up');
expect(r8.data?.type).toBe('api_call');
const r9 = await call('開票處理');
expect(r9.data?.type).toBe('api_call');
const r10 = await call('權(quán)限包');
expect(r10.data?.type).toBe('api_call');
});
數(shù)據(jù)預(yù)處理與向量庫的準(zhǔn)備工作
RAG應(yīng)用的知識庫準(zhǔn)備是實(shí)施過程中的關(guān)鍵環(huán)節(jié),涉及多個步驟和技術(shù)。以下是知識庫準(zhǔn)備的主要過程:
- 知識庫選擇:【全面性與質(zhì)量】數(shù)據(jù)源的信息準(zhǔn)確性在RAG應(yīng)用中最為重要,基于錯誤的信息將無法獲得正確的回答。
- 知識庫收集:【多類目數(shù)據(jù)】數(shù)據(jù)收集通常涉及從多個來源提取信息,包括不同的渠道,不同的格式等。如何確保數(shù)據(jù)最終可以形成統(tǒng)一的結(jié)構(gòu)并被統(tǒng)一消費(fèi)至關(guān)重要。
- 數(shù)據(jù)清理:【降低額外干擾】原始數(shù)據(jù)往往包含不相關(guān)的信息或重復(fù)內(nèi)容。
- 知識庫分割:【降低成本與噪音】將文檔內(nèi)容進(jìn)行分塊,以便更好地進(jìn)行向量化處理。每個文本塊應(yīng)適當(dāng)大小,并加以關(guān)聯(lián),以確保在檢索時能夠提供準(zhǔn)確的信息,同時避免生成噪聲。
- 向量化存儲:【Embedding生成】使用Embedding模型將文本塊轉(zhuǎn)換為向量表示,這些向量隨后被存儲在向量數(shù)據(jù)庫中,以支持快速檢索。
- 檢索接口構(gòu)建:【提高信息準(zhǔn)確性】構(gòu)建檢索模塊,使其能夠根據(jù)用戶查詢從向量數(shù)據(jù)庫中檢索相關(guān)文檔。
知識庫拆分
知識庫文檔的拆分顆粒度(Split Chunk Size) 是影響RAG應(yīng)用準(zhǔn)確性的重要指標(biāo):
- 拆分顆粒度過大可能導(dǎo)致檢索到的文本塊包含大量不相關(guān)信息,從而降低檢索的準(zhǔn)確性。
- 拆分顆粒度過小則可能導(dǎo)致必要的上下文信息丟失,使得生成的回答缺乏連貫性和深度。
- 在實(shí)際應(yīng)用中,需要不斷進(jìn)行實(shí)驗以確定最佳分塊大小。通常情況下,128字節(jié)大小的分塊是一個合適的分割大小。
- 同時還要考慮LLM的輸入長度帶來的成本問題。
下圖為得物開放平臺【開票取消預(yù)約上門取件】接口的接口文檔:
開票取消預(yù)約上門取件接口信息
拆分邏輯分析(根據(jù)理論提供128字節(jié)大小)
在成功獲取到對應(yīng)文本數(shù)據(jù)后,我們需要在數(shù)據(jù)的預(yù)處理階段,將文檔根據(jù)分類進(jìn)行切分。這一步將會將一份文檔拆分為多份文檔。
由上圖中信息可見,一個文檔的基礎(chǔ)結(jié)構(gòu)是由一級、二級標(biāo)題進(jìn)行分割分類的。一個基本的接口信息包括:基礎(chǔ)信息、請求地址、公共參數(shù)、請求入?yún)?、請求出參、返回參?shù)以及錯誤碼信息組成。
拆分方式
拆分的實(shí)現(xiàn)一般有2種,一是根據(jù)固定的文檔大小進(jìn)行拆分(128字節(jié))二是根據(jù)實(shí)際文檔結(jié)構(gòu)自己做原子化拆分。
直接根據(jù)文檔大小拆分的優(yōu)點(diǎn)當(dāng)然是文檔的拆分處理邏輯會直接且簡單粗暴,缺點(diǎn)就是因為是完全根據(jù)字節(jié)數(shù)進(jìn)行分割,一段完整的句子或者段落會被拆分成2半從而丟失語義(但可通過頁碼進(jìn)行鏈接解決)。
根據(jù)文檔做結(jié)構(gòu)化拆分的優(yōu)點(diǎn)是上下文結(jié)構(gòu)容易連接,單個原子文檔依舊具備語義化,檢索時可以有效提取到信息,缺點(diǎn)是拆分邏輯復(fù)雜具備定制性,拆分邏輯難以與其他知識庫復(fù)用,且多個文檔之間缺乏一定的關(guān)聯(lián)性(但可通過元信息關(guān)聯(lián)解決)。
在得物開放平臺的場景中,因為文檔數(shù)據(jù)大多以json為主(例如api表格中每個字段的名稱、默認(rèn)值、描述等),將這些json根據(jù)大小做暴力切分丟失了絕大部分的語義,難以讓LLM理解。所以,我們選擇了第二種拆分方式。
拆分實(shí)現(xiàn)
在文檔分割層面,Markdown作為一種LLM可識別且可承載文檔元信息的文本格式,作為向量數(shù)據(jù)的基礎(chǔ)元子單位最為合適。
基礎(chǔ)的文檔單元根據(jù)大標(biāo)題進(jìn)行文檔分割,同時提供frontmatter作為多個向量之間連接的媒介。
正文層面,開放平臺的API文檔很適合使用Markdown Table來做內(nèi)容承接,且Table對于大模型更便于理解。
根據(jù)以上這種結(jié)構(gòu),我們可得到以下拆分流程:
代碼實(shí)現(xiàn):
const hbsTemplate = `
---
服務(wù)ID (serviceId): {{ service.id }}
接口ID (apiId): {{ apiId }}
接口名稱 (apiName): {{ apiName }}
接口地址 (apiUrl): {{ apiUrl }}
頁面地址 (pageUrl): {{ pageUrl }}
---
# {{ title }}
{{ paragraph }}
`;
export const processIntoEmbeddings = (data: CombinedApiDoc) => {
const template = baseTemplate(data);
const texts = [
template(requestHeader(data)),
template(requestUrl(data)),
template(publicRequestParam(data)),
template(requestParam(data)),
template(responseParam(data)),
template(errorCodes(data)),
template(authPackage(data)),
].filter(Boolean) as string[][];
return flattenDeep(texts).map((content) => {
return new Document<MetaData>({
// id: toString(data.apiId!),
metadata: {
serviceId: data.service.id,
apiId: data.apiId!,
apiName: data.apiName!,
apiUrl: data.apiUrl!,
pageUrl: data.pageUrl!,
},
pageContent: content!,
});
});
};
知識庫導(dǎo)入
通過建立定時任務(wù)(DJOB),使用MILVUS sdk將以上拆分后的文檔導(dǎo)入對應(yīng)數(shù)據(jù)集中。
CO-STAR結(jié)構(gòu)
在上文中的Prompt,使用了一種名為CO-STAR的結(jié)構(gòu)化模板,該框架由新加坡政府科技局的數(shù)據(jù)科學(xué)與AI團(tuán)隊創(chuàng)立。CO-STAR框架是一種用于設(shè)計Prompt的結(jié)構(gòu)化模板,旨在提高大型語言模型(LLM)響應(yīng)的相關(guān)性和有效性,考慮了多種影響LLM輸出的關(guān)鍵因素。
結(jié)構(gòu):
- 上下文(Context):提供與任務(wù)相關(guān)的背景信息,幫助LLM理解討論的具體場景,確保其響應(yīng)具有相關(guān)性。
- 目標(biāo)(Objective):明確你希望LLM執(zhí)行的具體任務(wù)。清晰的目標(biāo)有助于模型聚焦于完成特定的請求,從而提高輸出的準(zhǔn)確性。
- 風(fēng)格(Style):指定希望LLM采用的寫作風(fēng)格。這可以是某位名人的風(fēng)格或特定職業(yè)專家的表達(dá)方式,甚至要求LLM不返回任何語氣相關(guān)文字,確保輸出符合要求。
- 語氣(Tone):設(shè)定返回的情感或態(tài)度,例如正式、幽默或友善。這一部分確保模型輸出在情感上與用戶期望相符。
- 受眾(Audience):確定響應(yīng)的目標(biāo)受眾。根據(jù)受眾的不同背景和知識水平調(diào)整LLM的輸出,使其更加適合特定人群。
- 響應(yīng)(Response):規(guī)定輸出格式,以確保LLM生成符合后續(xù)使用需求的數(shù)據(jù)格式,如列表、JSON或?qū)I(yè)報告等。這有助于在實(shí)際應(yīng)用中更好地處理LLM的輸出。
在上文結(jié)構(gòu)化的實(shí)現(xiàn)中,演示了如何使用CO-STAR結(jié)構(gòu)的Prompt,要求大模型“冰冷的”對用戶提問進(jìn)行的解析,當(dāng)然CO-STAR也適用于直接面向用戶的問答,例如:
## Context
我是一名正在尋找酒店信息的旅行者,計劃在即將到來的假期前往某個城市。我希望了解關(guān)于酒店的設(shè)施、價格和預(yù)訂流程等信息。
## Objective
請?zhí)峁┪宜璧木频晷畔?,包括房間類型、價格范圍、可用設(shè)施以及如何進(jìn)行預(yù)訂。
## Style
請以簡潔明了的方式回答,確保信息易于理解。
## Tone
使用友好和熱情的語氣,給人一種歡迎的感覺。
## Audience
目標(biāo)受眾是普通旅行者,他們可能對酒店行業(yè)不太熟悉。
## Response
請以列表形式呈現(xiàn)每個酒店的信息,包括名稱、地址、房間類型、價格和聯(lián)系方式。每個酒店的信息應(yīng)簡短且直接,便于快速瀏覽。
相似性搜索
當(dāng)我們使用了問題結(jié)構(gòu)化Runnable后,非開放平臺類問題將會提前終止,告知用戶無法解答相關(guān)問題,其他有效回答將會進(jìn)入相似性搜索環(huán)節(jié)。
相似性搜索基于數(shù)據(jù)之間的相似性度量,通過計算數(shù)據(jù)項之間的相似度來實(shí)現(xiàn)檢索。在答疑助手的相似性實(shí)現(xiàn)是通過余弦相似度來進(jìn)行相似性判斷的。
我們將用戶的提問,與向量數(shù)據(jù)庫中數(shù)據(jù)進(jìn)行余弦相似度匹配。取K為5獲取最相似的五條記錄。
注意:此K值是經(jīng)過一系列的推斷最終決定的,可根據(jù)實(shí)際情況調(diào)整。
import { Milvus } from '@langchain/community/vectorstores/milvus';
import { OpenAIEmbeddings } from '@langchain/openai';
import { RunnableSequence } from '@langchain/core/runnables';
import { getLLMConfig } from 'src/utils/llm/get-llm-config';
export const $getContext = async () => {
const embeddings = new OpenAIEmbeddings(
getLLMConfig().OpenAIEmbeddingsConfig,
);
const vectorStore = await Milvus.fromExistingCollection(embeddings, {
collectionName: 'open_rag',
});
return RunnableSequence.from([
(input) => {
return input.question;
},
vectorStore.asRetriever(5),
]);
};
此Runnable會將搜索結(jié)果組成一大段可參考數(shù)據(jù)集,用于后續(xù)用戶提問。
用戶提問解答
用戶提問的解答同樣通過Runnable的方式來承接,通過用戶提問、結(jié)構(gòu)化數(shù)據(jù)、提取的相似性上下文進(jìn)行結(jié)合,最終得到問題的解答。
我們先將上下文進(jìn)行格式化整理:
import { RunnablePassthrough, RunnablePick } from '@langchain/core/runnables';
import { Document } from 'langchain/document';
import { PromptTemplate } from '@langchain/core/prompts';
import { MetaData } from 'src/types';
const $formatRetrieverOutput = async (documents: Document<MetaData>[]) => {
const strings = documents.map(async (o) => {
const a = await PromptTemplate.fromTemplate(`{pageContent}`).format({
pageContent: o.pageContent,
});
return a;
});
const context = (await Promise.all(strings)).join('\n');
return context;
};
export const $contextAssignRunnable = () => {
return RunnablePassthrough.assign({
context: new RunnablePick('context').pipe($formatRetrieverOutput),
});
};
問答整體Prompt實(shí)現(xiàn):
export const promptTemplateMarkdown = () => {
return `
# CONTEXT
得物的開放平臺是一個包含著 API 文檔,解決方案文檔的平臺,商家可以通過這個平臺獲取到得物的各種接口,以及解決方案,幫助商家更好的使用得物的服務(wù)。
現(xiàn)在得物開放平臺的人工答疑率相當(dāng)高,原因可能是文檔的信息藏的較深,我希望做一個人工智能答疑助手,通過分析開放平臺的各種文檔,來回答用戶的問題,最終讓用戶不進(jìn)入人工答疑階段。
我們只討論[開放平臺接口]的相關(guān)問題,不要談及其他內(nèi)容。
# OBJECTIVE
你需要根據(jù)用戶的輸入,以及提供的得物開放平臺的文檔上下文,進(jìn)行答疑。
你只接受有關(guān)[開放平臺接口]的相關(guān)問答,不接受其余任何問題。
## 關(guān)于用戶的輸入:
1. 你會得到一份符合 JSONSchema 結(jié)構(gòu)的結(jié)構(gòu)化數(shù)據(jù),這份數(shù)據(jù)我會使用\`<structured-input></structured-input>\`包裹。
這份結(jié)構(gòu)化數(shù)據(jù)是通過實(shí)際的用戶提問進(jìn)行了二次分析而得出的。結(jié)構(gòu)化數(shù)據(jù)里也會包含用戶的最初始的問題供你參考(最初始的問題會放在 question 字段里)
## 關(guān)于上下文
1. 我已經(jīng)提前準(zhǔn)備好了你需要參考的資料,作為你回答問題的上下文,上下文是由許多篇 Markdown 文檔組成的。這些 Markdown 的文檔大標(biāo)題代表了這個片段的模塊名,例如 \`# 接口入?yún)`就代表這部分是文檔的接口入?yún)⒉糠郑?\`# 接口返回\`就代表這部分是文檔的接口返回部分,
2. 上下文中的主要信息部分我會使用 Markdown Table 的結(jié)構(gòu)提供給你。
3. 每個上下文的開頭,我都會給你一些關(guān)于這份上下文的元信息(使用 FrontMatter 結(jié)構(gòu)),這個元信息代表了這份文檔的基礎(chǔ)信息,例如文檔的頁面地址,接口的名稱等等。
以下是我提供的結(jié)構(gòu)化輸入,我會使用\`<structured-input></structured-input>\`標(biāo)簽做包裹
<structured-input>
{structuredInput}
</structured-input>
以下是我為你提供的參考資料,我會使用\`<context></context>\`標(biāo)簽包裹起來:
<context>
{context}
</context>
# STYLE
你需要把你的回答以特定的 JSON 格式返回
# TONE
你是一個人工智能答疑助手,你的回答需要溫柔甜美,但又不失嚴(yán)謹(jǐn)。對用戶充滿了敬畏之心,服務(wù)態(tài)度要好。在你回答問題之前,需要簡單介紹一下自己,例如“您好,很高興為您服務(wù)。已經(jīng)收到您的問題?!?
# AUDIENCE
你的用戶是得物開放平臺的開發(fā)者們,他們是你要服務(wù)的對象。
# RESPONSE
你返回的數(shù)據(jù)結(jié)構(gòu)必須符合我提供的 JSON Schema 規(guī)范,我給你的 Schema 將會使用\`<structured-output-schema></structured-output-schema>\`標(biāo)簽包裹.
<structured-output-schema>
{strcuturedOutputSchema}
</structured-output-schema>
`;
};
以上問答通過CO-STAR結(jié)構(gòu),從6個方面完全限定了答疑助手的回答腔調(diào)以及問答范疇,我們現(xiàn)在只需要準(zhǔn)備相應(yīng)的數(shù)據(jù)結(jié)構(gòu)提供給這份Prompt模板。
問答結(jié)果結(jié)構(gòu)化
在開放平臺答疑助手的場景下,我們不僅要正面回答用戶的問題,同時還需要給出相應(yīng)的可閱讀鏈接。結(jié)構(gòu)如下:
import { z } from 'zod';
const zOutputSchema = z
.object({
question: z
.string()
.describe(
'提煉后的用戶提問。此處的問題指的是除去用戶提供的接口信息外的問題。盡量多的引用用戶的提問',
),
introduction: z
.string()
.describe('開放平臺智能答疑助手對用戶的問候以及自我介紹'),
answer: z
.array(z.string())
.describe(
'開放平臺智能答疑助手的回答,需將問題按步驟拆分,形成數(shù)組結(jié)構(gòu),回答拆分盡量步驟越少越好。如果回答的問題涉及到具體的頁面地址引用,則將頁面地址放在relatedUrl字段里。不需要在answer里給出具體的頁面地址',
),
relatedUrl: z
.array(z.string())
.describe(
'頁面的鏈接地址,取自上下文的pageUrl字段,若涉及多個文檔,則給出所有的pageUrl,若沒有pageUrl,則不要返回',
)
.optional(),
})
.required({
question: true,
introduction: true,
answer: true,
});
type OpenRagOutputType = z.infer<typeof zOutputSchema>;
export { zOutputSchema, type OpenRagOutputType };
在我們之前的設(shè)計中,我們的每一份向量數(shù)據(jù)的頭部,均帶有相應(yīng)的文檔meta信息,通過這種向量設(shè)計,我們可以很容易的推算出可閱讀鏈接。同時,我們在這份zod schema中提供了很詳細(xì)的description,來限定機(jī)器人的回答可以有效的提取相應(yīng)信息。
Runnable的結(jié)合
在用戶提問解答這個Runnable中,我們需要結(jié)合Retriever, 上下文,用戶提問,用戶輸出限定這幾部分進(jìn)行組合。
import { ChatOpenAI } from '@langchain/openai';
import { $getPrompt } from './prompt/index';
import { JsonOutputParser } from '@langchain/core/output_parsers';
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
import { zOutputSchema } from './schema';
import { $getContext } from './retriever/index';
import { getLLMConfig } from 'src/utils/llm/get-llm-config';
import { getStringifiedJsonSchema } from 'src/utils/llm/get-stringified-json-schema';
import { n } from 'src/utils/llm/gen-runnable-name';
const b = n('$open-rag');
type OpenRagInput = {
structuredInput: string;
question: string;
};
const $getOpenRag = async () => {
const $model = new ChatOpenAI(getLLMConfig().ChatOpenAIConfig).bind({
response_format: {
type: 'json_object',
},
});
const chain = RunnableSequence.from([
RunnableMap.from<OpenRagInput>({
// 問答上下文
context: await $getContext(),
// 結(jié)構(gòu)化輸入
structuredInput: (input) => input.structuredInput,
// 用戶提問
question: (input) => input.question,
// 輸出結(jié)構(gòu)
strcuturedOutputSchema: () => getStringifiedJsonSchema(zOutputSchema),
}).bind({ runName: b('runnable-map') }),
$getPrompt().bind({ runName: b('prompt') }),
$model,
new JsonOutputParser(),
]).bind({ runName: b('chain') });
return chain;
};
export { $getOpenRag };
流程串聯(lián)
通過上文的幾大部分,我們已經(jīng)將 用戶提問、結(jié)構(gòu)化解析、相似性搜索、問答上下文,問答Prompt模板以及結(jié)構(gòu)化輸出這幾部分實(shí)現(xiàn)完成。
現(xiàn)在可以通過LangChain的Runnable能力進(jìn)行流程串聯(lián)。
const structure = safeJsonParse<StructuredInputType>(
await $getStructuredInput().invoke({ question }),
null,
)
const mainChain = RunnableSequence.from<{ question: string }, OpenRagOutputType>([
RunnablePassthrough.assign({
structuredInput: () => structure,
}),
await $getOpenRag(),
]);
const response = await mainChain.invoke({
question: question,
});
串聯(lián)流程如下:
返回結(jié)果示例:
輸入:
{
"question": "正式環(huán)境調(diào)用:直發(fā)訂單獲取打印面單v2【品牌直發(fā)/眾籌直發(fā)/拍賣直發(fā)】接口返回報錯(https://openapi.dewu.com/dop/api/v2/order/brand_deliver/express_sheet)。請求參數(shù):{\"order_no\":\"110152014363632294\",\"app_key\":\"953a43bec40943c98a5cdbc63333242f;\",\"sign\":\"AD968ED6A359D144B61F1FEF6BB380ED\",\"timestamp\":1720430064169}"
}
輸出:
{
"question": "我的問題是:正式環(huán)境調(diào)用:直發(fā)訂單獲取打印面單v2【品牌直發(fā)/眾籌直發(fā)/拍賣直發(fā)】接口返回報錯。",
"introduction": "您好,很高興為您服務(wù)。已經(jīng)收到您的問題。",
"answer": [
"根據(jù)您提供的請求參數(shù)信息,出現(xiàn)錯誤可能是由于接口調(diào)用參數(shù)不正確導(dǎo)致的,接下來我們一步步排查問題。",
"首先,您提供的請求參數(shù)中的訂單號為\"110152014363632294\",請確認(rèn)該訂單號的狀態(tài)是否為待物流攬收或待買家收貨狀態(tài)。",
"其次,檢查您所使用的app_key是否正確,確保該app_key授權(quán)了調(diào)用該接口的權(quán)限。",
"再次,請確認(rèn)傳入的簽名(sign)是否正確,可能需要重新生成簽名以保證準(zhǔn)確性。",
"最后,檢查時間戳(timestamp)是否正確,并且處于合理的時間范圍內(nèi)。",
"如果以上步驟都沒有解決問題,建議您查看詳細(xì)的接口文檔以進(jìn)一步調(diào)試和解決問題。"
],
"relatedUrl": [
"https://open.dewu.com/#/api?apiId=1174"
]
}
五、應(yīng)用調(diào)試
基于大模型應(yīng)用可能設(shè)計到多個Runnable的多次調(diào)用,借用LangSmith的trace功能,我們可以對每一個Runnable進(jìn)行出入?yún)⒌膁ebug。
關(guān)于LangSmith的接入:
六、未來展望
RAG在減少模型幻覺,無需模型訓(xùn)練就可享受內(nèi)容時效性的特點(diǎn)在此類答疑應(yīng)用中展露無遺,RAG應(yīng)用開放平臺落地從一定程度上驗證了依賴可靠知識庫的答疑場景具備可執(zhí)行性,還為內(nèi)部系統(tǒng)的應(yīng)用提供了有力的參考。在實(shí)際應(yīng)用中,除了直接解決用戶的提問外,通過回放用戶提問的過程,可以為產(chǎn)品和業(yè)務(wù)的發(fā)展提供重要的洞察。
面向未來,是否可以嘗試將答疑助手的形式在內(nèi)部系統(tǒng)落地,在內(nèi)部建立知識庫體系,將部分問題前置給大模型處理,降低TS和開發(fā)介入答疑的成本。
本文轉(zhuǎn)載自 ??得物技術(shù)??,作者: 惑普
