XChat 是界面,Runner 是执行服务,XSLM 是模型 。客户端 给问题加格式,通过HTTP 获取回答。
初版在浏览器 终端 运行,每题独立,不发送历史。下一步可在小上下文 限制内增加历史。重点是请求真正经过自己训练 的模型。
请求经过的路——从 XChat 到权重
在 XChat 输入 hi 并按 Enter,会依次发生这些事。
xchat.py 把问题包成训练格式。"Q: hi" 的下一行放 "A: "。
生成 JSON {"prompt ": …, "tokens ": 24},POST 到 http://127.0.0.1:8765/generate。
Runner 的 do_POST 读取并检查正文,然后调用 generate。
generate 用分词器 生成编号,用内存 中的 GPT 逐个抽取词元,在换行 处停止。
Runner 用 {"text": …} 响应,xchat.py 把它显示在 XSLM> 后面。
这条路上任何一处断了,对话就进行不了。所以出问题时,沿着这条路逐一检查:Runner 是否在运行(curl /health ),能否生成(curl /generate),XChat 的地址和端口 是否正确。
XChat 客户端——输入循环与 HTTP 请求
xchat.py 分两部分。
ask(question):发送一个请求并返回回答。用 Request 构造包含地址、正文、头部的 POST 请求,用 urlopen 发送,再用 json.load 读取响应正文,取出 text。timeout=120 表示 2 分钟内没有回答就放弃。为了在慢速模拟器 上能等长时间的生成,设得比较宽裕。
输入循环 :用 while True 不断显示 You>,逐行读取。/exit 就结束,空行跳过,其余交给 ask,在 XSLM> 后输出回答。
每个问题都重新读取 settings.json。提示词格式("Q: " … "A: ")由客户端添加。如果做成由服务器 添加格式,客户端就不需要知道格式。无论哪种,格式都只能在一个地方决定,才不会错位。
处理错误——不编造假回答
xchat.py 这样分类处理错误。
EOFError、KeyboardInterrupt:按了 Ctrl+D (输入结束)或 Ctrl+C 。结束循环,回到 bash 。
URLError:没能连接 Runner(Connection refused 等)。Runner 没开,或端口不对。
HTTPError:连上了,但 Runner 用 400 这样的错误码回答。它是 URLError 的一种,所以在同一行被捕获。
TimeoutError:在规定时间内没有回答。
无论哪种情况,都显示 "Runner: 错误内容",然后等待下一个问题。连不上时,不用事先准备好的句子来掩盖。这样才能马上知道哪里坏了。关掉 Runner 再提问试试(应该出错才正常),再启动它,确认对话恢复。
附上对话记录——上下文长度的限制
现在的 XChat 每个问题都是独立的。模型不知道前面说过的话。真实聊天机器人 看起来记得前面的对话,是因为每次都把过去的整个对话放到提示词前面一起发送。模型本身没有记忆 。
用 XSLM 试试,很快就会碰壁。下面的实验在一轮对话(Q: hi / A: hi friend.)后面接上新问题 cat。提示词有 21 个词元,但模型只看最后 12 个词元,实际看到的文字从 "iend." 开始。前面的对话被截掉了。
所以要附上对话记录 ,还要一起做这些事。
从最旧的对话开始丢弃,使其放进上下文长度内。
新训练一个 context 更大的模型(位置表变大,计算变慢)。
训练资料里也要放入多轮连续的对话,模型才能学会那种形状。
下一个挑战:在 xchat.py 中设一个 history 列表 ,把过去的问答附上一起发送。数一数词元数,超过 12 就丢掉前面的部分。
问问人工智能
动手验证 lab/context.py
import json
from pathlib import Path
from tokenizer import BPE
tok = BPE(**json.loads(Path('data/tokenizer.json').read_text()))
size = json.loads(Path('settings.json').read_text())['context']
history = 'Q: hi\nA: hi friend.\nQ: cat\nA: '
ids = tok.encode(history)
print(len(ids), 'tokens; the model sees the last', size)
print(repr(tok.decode(ids[-size:])))
cd ~/xslm
mkdir -p lab
micro lab/context.py
python3 -m lab.context
运行结果
21 tokens; the model sees the last 12
'iend.\nQ: cat\nA: '
真实的对话模型怎么做——预训练之后的阶段
XSLM 用 Q/A 格式的文字从零训练。ChatGPT 、Claude 这样的对话模型通常要经过几个阶段。
预训练 (pre-training):用海量文字做下一个词元预测 。和 XSLM 的 train.py 做的事相同,只是规模不同。
指令训练(SFT , instruction tuning):用问题和好答案的对再训练,养成遵循指令的习惯。区分用户和助手轮次的对话格式(聊天模板 )也在这时学会。
偏好训练(RLHF 等):调整模型,使其偏向人们选出的更好的回答。
需要最新信息时,不重新训练,而用 RAG 。用问题检索出文档,一起放进提示词,模型看着这些文档回答。在资料格式中设置像 [DOC] 这样的文档栏,事先学会参考文档来回答的形状,以后就能把检索结果放进那一栏使用。
问问人工智能
下一个挑战——让 XSLM 变大的方法
用完成的 XSLM 可以做的实验。每次只改一项,记录损失 和回答。
增加资料:问题用多种说法,答案用相同格式,写几十到几百行。
增加训练:改变学习率 和训练次数,比较训练和验证损失 。
扩大模型:把 width 改成 16,context 改成 24。测量参数 数量和每一步的时间如何变化。
叠块:把 model.py 的注意力 + MLP 封装成函数 ,每个块各自建权重 ,让它经过两次。
分头:把宽度 8 分成每头 4 格的两个头,分别做注意力后再拼接。
加 top-p :在 generate 中做一个按累积概率 截断候选的选项。
流式 输出和对话记录:加到 Runner 和 XChat 上试试。
能做到这里,就在自己电脑上用 PyTorch 重写同样的结构。用张量 计算,就能快几千倍,训练几千万个参数。Andrej Karpathy 的 nanoGPT 、build-nanogpt 也是很好的下一步教材。
动手输入并实验
保持Runner运行,编写并运行xchat.py,在You>输入hi。先/exit回到bash,再执行kill 。
cd ~/xslm
micro xchat.py
python3 xchat.py
hi
/exit
kill "$(cat runner.pid)"
在终端中练习
xchat.py 需要亲手编写的完整源码
input读取问题,Request发送JSON POST,urlopen读取真实响应;失败时显示错误。
import json
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
def ask(question):
cfg = json.loads(Path('settings.json').read_text())
body = {'prompt': 'Q: ' + question + '\nA: ', 'tokens': cfg['tokens']}
request = Request('http://127.0.0.1:' + str(cfg['port']) + '/generate',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
with urlopen(request, timeout=120) as response:
return json.load(response)['text']
if __name__ == '__main__':
print('XChat -> Runner -> XSLM | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSLM>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
逐行拆解代码
xchat.py ask——发送一个问题
def ask(question):
cfg = json.loads(Path('settings.json').read_text())
body = {'prompt': 'Q: ' + question + '\nA: ', 'tokens': cfg['tokens']}
request = Request('http://127.0.0.1:' + str(cfg['port']) + '/generate',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
with urlopen(request, timeout=120) as response:
return json.load(response)['text']
每个问题都读取 settings.json,获得词元数和端口。把问题包成 "Q: " + 问题 + 换行 + "A: ",对齐训练格式。给 Request 传入 data 就成为 POST,正文是 JSON 字节 ,头部是 Content-Type 。with urlopen(…) 读完响应后会关闭连接。json.load 把响应读成字典 ,ask 只返回 text。
xchat.py 对话循环
if __name__ == '__main__':
print('XChat -> Runner -> XSLM | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSLM>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
输出开场提示,用 while True 接收问题。input 显示 You>,读取一行直到 Enter,用 strip 去掉首尾空格。/exit 就用 break 结束,空问题跳过。Ctrl+D 以 EOFError、Ctrl+C 以 KeyboardInterrupt 进来,同样结束。Runner 连接错误和超时会告知,然后等待下一个问题。
检查理解
展示XChat→Runner→权重→生成文本。停Runner应报连接错误,重启后恢复。
遇到问题时 不要在XChat里输入kill,它会成为问题。先退出,完成后导出项目。
常见问题
在 XChat 里输入了 kill,Runner 却没关掉。
在 You> 输入的文字不是命令 ,而是作为问题发给 Runner。先用 /exit 或 Ctrl+D 结束 XChat,回到 bash 提示符($),再输入 kill 命令。
出现 Runner: <urlopen error [Errno 111] Connection refused>。
Runner 没开,或者还在启动中。用 cat runner.log 确认启动信息(XSLM Runner: …),看 curl -s http://127.0.0.1:8765/health 是否有回应,然后再提问。如果刷新过页面,需要重新启动 Runner。
回答和问题无关。是连接出错了吗?
只要 XSLM> 后面出现了任何生成的文字,连接就是对的。内容是模型的实力。用 python3 generate.py --prompt 直接运行同一个问题试试。设置和 seed 相同,结果会和经过 Runner 的回答完全一样。