ChatGPT:和黑客知識庫聊天
chatgpt可以很好的解決通用型問題,但是對于垂直專業領域的問題還不夠好,應該還需要再進行訓練。
之前發過一個想法

周末做了幾種可行性的探測。
可行性研究
prompt上下文關聯
直接再prompt的上下文中輸入問題和引用的原文,再由chatgpt生成答案。

這種方法的缺點就是每次輸入的文字有限制"4096 token"。
值得一提的是費用很便宜。一百萬token才2美元。

有一些突破限制方法:
- 例如把一個長的prompt拆成多個短的,逐個執行后再拼接起來。但是openai是按照token數量進行收費的,不適合用于數據量可能很大的情況。
- 將文本放到一個網頁,讓chatgpt去讀,但是有時候請求會讀不到,無法保證可用性

Fine-tuning (微調)
openai提供模型的微調服務:https://platform.openai.com/docs/guides/fine-tuning
GPT-3 已經在來自開放互聯網的大量文本上進行了預訓練。當給出僅包含幾個示例的提示時,它通常可以憑直覺判斷出您要執行的任務并生成合理的完成。這通常稱為“小樣本學習”。
微調通過訓練比提示中更多的示例來改進小樣本學習,讓您在大量任務中取得更好的結果。「對模型進行微調后,您將不再需要在提示中提供示例。」 這樣可以節省成本并實現更低延遲的請求。
它只需要三個步驟
- 準備和上傳訓練數據
- 訓練新的微調模型
- 使用您的微調模型
數據格式形如
{"prompt": "", "completion": ""}
{"prompt": "", "completion": ""}
{"prompt": "", "completion": ""}
需要對每個數據的原文編寫一個可用prompt。

微調模型里沒有gpt3.5的,都是基于gpt3.0, 最好的是davinci,價格也比較貴,訓練,使用都是按token收費。
其他可替代性模型
開源的大語言模型也有很多,它們也開放了自己的預訓練模型,但是需要你自己訓練進行微調才能達到效果,成本比較高,需要自己整理數據集,用GPU訓練。
谷歌開源的語言大模型flan-t5
https://huggingface.co/google/flan-t5-xxl
百度開源的npl工具集
https://github.com/PaddlePaddle/PaddleNLP
GPT-J 6B, OPT, GALACTICA, GPT-Neo集成測試方案
https://github.com/oobabooga/text-generation-webui
GPT3中文預訓練模型
https://modelscope.cn/models/damo/nlp_gpt3_text-generation_30B/summary
embedding
基于語義檢索,所謂語義檢索(也稱基于向量的檢索,如上圖所示),是指檢索系統不再拘泥于用戶 Query 字面本身,而是能精準捕捉到用戶 Query 后面的真正意圖并以此來搜索,從而更準確地向用戶返回最符合的結果。
通過使用語義索引模型找到文本的向量表示,在高維向量空間中對它們進行索引,并度量查詢向量與索引文檔的相似程度,從而解決了關鍵詞索引帶來的缺陷。
openai也開放了這部分api。https://platform.openai.com/docs/guides/embeddings/what-are-embeddings
它的api價格比較低

1000萬token用來索引只消耗4美元。
官方也提供了搜索相似代碼,搜索文本的案例
https://github.com/openai/openai-cookbook/blob/main/examples/Semantic_text_search_using_embeddings.ipynb
https://github.com/openai/openai-cookbook/blob/main/examples/Code_search.ipynb
大概步驟是:
通過調用text-embedding-ada-002模型,輸出文本向量,后續使用cos余弦相似度或其他算法找到相似的內容。
下文就是對這塊的實驗。
數據集預處理
www.hacking8.com和i.hacking8.com有很多安全方向的數據,數據源不用發愁。
我們調用openai的embedding對文本進行語義化向量輸出,需要將一篇大文章分成多個塊,每個塊最大8191token(openai限制)。
在實驗中發現,數據的預處理很重要,決定了數據的質量。每個塊分多大,塊中的上下文最好關聯而不是錯開。
決定先對 https://www.hacking8.com/sqlmap-parse/ 這篇文章索引看看效果。

這本書的內容源格式是markdown的,可以通過拆分markdown的標題來作為塊,我為了方便之后通用其他數據集,用5行作為索引的塊。
import glob
import math
import os.path
from itertools import islice
import numpy as np
import openai
import pandas as pd
import tiktoken
token = "sk-xxxx"
os.environ.setdefault("OPENAI_API_KEY", token)
openai.api_key = token
def num_tokens_from_string(string: str, encoding_name: str = 'cl100k_base') -> int:
"""Returns the number of tokens in a text string."""
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
return num_tokens
def get_embedding(text, model="text-embedding-ada-002"):
text = text.replace("", " ")
return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']
data = []
def save_csv(path, url, content, index):
data.append({
"path": path,
"url": url,
"content": content,
"index": index
})
# 創建數據集
base = "/books"
for filename in glob.glob('/books/sqlmap-parse/*.md', recursive=True):
with open(filename, 'r') as f:
path = os.path.relpath(filename, base)
url = "https://www.hacking8.com/" + path
content = f.read().replace("", "").strip()
contents = content.split("")
rawSize = 5
rawLength = len(contents)
index = 0
for i in range(math.ceil(rawLength / rawSize)):
index += 1
current = contents[i * rawSize:(i + 1) * rawSize]
cc = "".join(current).strip()
num = num_tokens_from_string(cc)
if num > 2000:
it = iter(cc)
while batch := tuple(islice(it, 2000)):
index += 1
cc = "".join(batch)
save_csv(path, url, cc, index)
else:
save_csv(path, url, cc, index)
table = pd.DataFrame(data=data, columns=["path", "url", "index", "content"])
print(table)
table.to_csv("test.csv")
最終得到一個1000多行的CSV。將結果保存到test.csv上

計算文本向量
再預處理數據基礎上新增一列,調用openai計算embedding。
openai的api有限制,有時候會報錯,所以加了一個異常捕獲代碼,代碼有判斷,有embedding內容則跳過,沒有則進行計算,多運行幾次,直至全部計算成功。
# 讀取數據集,添加集合向量
# 讀取CSV文件
df = pd.read_csv('test.csv')
df["embedding"] = ""
# 遍歷每一行并修改列值
for index, row in df.iterrows():
content = row["content"]
embedding = row["embedding"]
is_null = pd.isnull(embedding)
if not is_null:
continue
print(index)
try:
embedding = get_embedding(content)
time.sleep(1) // openai有 60/min 的限制
except Exception as e:
print(e)
continue
s = "[" + ", ".join(str(x) for x in embedding) + "]"
df.loc[index, "embedding"] = s
if int(index) % 20 == 0:
df.to_csv('test2.csv', index=False)
df.to_csv('test2.csv', index=False)
問答
計算問題的語義向量,并和數據庫中對比相似度,取排行前三的內容作為prompt,再調用gpt3.5做最后總結。
df = pd.read_csv('test2.csv')
def search_reviews(df, query, n=3):
embedding = get_embedding(query, model='text-embedding-ada-002')
df['similarities'] = df.embedding.apply(lambda x: cosine_similarity(eval(x), embedding))
res = df.sort_values('similarities', ascending=False).head(n)
return res
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
prompt = '''你是一個大型語言模型,你的專長是閱讀和總結文章。給你一個查詢和一系列來自文本的輸入,按照它們與查詢的余弦相似度排序。你必須接受給定的輸入,并返回非常詳細的回答摘要。following embeddings as data: '''
query = "sqlmap的延時注入如何做的"
answer = search_reviews(df, query, 3)
url_set = set()
for index, item in answer.iterrows():
content = item.content
url = item.url
url_set.add(url)
# print("參考:", content)
prompt += "{}:{}".format(index, content) + ""
resp_raw = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": "Question:" + query},
]
)
import json
resp = json.loads(str(resp_raw))
content = resp["choices"][0]["message"]["content"]
print("答案", content)
print("參考鏈接", url_set)
我詢問它 “sqlmap的延時注入如何做的”,它最終返回

答案 sqlmap的延時注入是基于時間的注入。其邏輯是當延時選項開啟的時候,sqlmap會訪問30次網頁,并存儲和上一次訪問的間隔,所以總共會保存有30次的時間間隔。之后會生成三個不同的數字,并有這些數字組成不同的邏輯,將這些邏輯替換測試向量中原有的邏輯,并觀察響應是否如預期。如果響應與預期一致,則sqlmap認為注入存在。
參考鏈接 {'https://www.hacking8.com/sqlmap-parse/2.html', 'https://www.hacking8.com/sqlmap-parse/10.html'}
回答挺不錯的,簡潔和有效
總結
這是一個簡單例子,如何將知識庫做索引,以及用口語化的方式查詢。
還有一些可以優化的點:
- 人工分塊,現在分塊比較粗暴,會導致很多代碼被無效分塊,人工分塊標記代碼,這樣生成的向量會比較準確
- 快速檢索相似向量

- 目前的比對是全量數據比對,以后索引hacking8的全量數據的時候,再全量比對速度就很慢了,可以用 https://github.com/facebookresearch/faiss,它使用機器學習的手段,會將所有向量先進行一次聚類,相似的放到一起,相當于將相似的做了索引,再查詢時就根據聚類的去查,就很快了。
后續將繼續各類技術文檔的索引,希望能達到一個目標,技術將不再看文檔,而是直接問問題。
代碼和數據集放在知識星球了