现在,让我们打造属于你自己的人工智能吧。
在此浏览器中亲手输入代码,完成数据准备、小型GPT训练、Runner和XChat。不使用预训练权重或外部AI API。
XSML概述
先了解整个开发过程。
XSML意为Extra Small ,是SONUB为学习开发创造的名称,读作엑세스엠엘。通常的首字母顺序为XSLM,本课程按指定名称使用XSML。目标是完成全过程,而非流利回答。
文本变成编号,GPT学习预测下一个编号,加载,XChat发送问题。每个部分都由你编写;提供计算和文件功能。
动手输入并实验
打开输入命令,说明五个部分的作用。短英文示例以后可换成自己的语言。
python3 -c "print('XSML: text -> tokens -> GPT -> Runner -> XChat')"
检查理解
自己、保存、重新加载权重并用于XChat,就完成了整个链路。
遇到问题时
初学者可先复习Python变量、列表、循环、函数、类,或做二元模型热身。
可选热身:用二元模型数词
通向亲手开发的计划
一开始像玩具一样做得小小的,理解原理之后再逐步做大。
收集句子
收集你想学习的主题的句子。一开始只有 10 个句子也可以。
统计下一个词的次数
制作一张表,记录某个词后面各个词出现了多少次。
计算概率分数
数一数选中的词后面紧跟着的每个词各出现了几次——句子结束也算一次。然后用每个次数除以总次数。如果“天空是”后面跟着“蓝色的”三次、“灰色的”一次,那么“蓝色的”就是 3 ÷ 4 = 75%,“灰色的”是 25%。
比较两种选择方法
每次都选分数最高的候选(贪心选择),或者按比例抽签(采样)——抽签也可能再次抽到同一个词。抽到句子结束,就停下来。这一步里,没有任何地方会检查这句话是不是真的。
按自己的标准改进
加入更好的句子、更多的数据和不同的选择规则,把它培养成属于自己的人工智能。
第一次来?请按这些步骤做
这个实验会做一个非常小的,它只看前面一个词。它统计哪个词后面跟着哪个词,把这些次数变成,再掷一颗按次数加权的骰子来选下一个词。
- 句子保持不变,点击“再次生成”。会出现一个新句子,里面只有训练文本中的词。
- 看概率公式:下一个词跟在前一个词后面的次数,除以前一个词出现的次数,就是概率。
- 在计数表里,每一行是前一个词,每一列是下一个词。格子颜色越深,这一对出现得越多。
- 把选择方式换成,再点一次:每次都会得到同样的句子。换回并调高——句子会变得越来越奇怪。
- 在训练文本里加一句你自己的句子。表格和概率马上就会变,因为这个就只是这些次数。
想更进一步? 用 Python 亲手做一个(AI 课程) 下一个词是怎么选出来的
小小的概率计算器
表中的百分比是温度为 1 时的值。温度只改变每个词被抽中的可能性,不改变计数,也不改变内容是否真实。
起始句子最后一个词后面还没有候选词。
| 前一个词下一个词 | |
|---|---|
词太多了,只显示最先出现的 8 个
核心概念
思路很简单。读很多句子,数一数哪个词后面紧跟着哪个词——句子在哪里结束,也一起算上。这些次数就成了一张小表,光靠这张表,就已经能猜出下一个词。你在这一页要做的模型,就是这个。
真正的也靠同样的思路运转,只是规模大得多。它不用一张小小的次数表,而是从例子中学会几十亿个数字,这些数字叫作。它看的也不只是前面那一个词,而是你的整个问题和到目前为止的对话。它也不是去查一句存好的句子。不过,最后一步和你做的一样——给每个候选打分,然后选一个。
本实验与真正的语言模型对比
| 本实验(二元模型) | 神经语言模型 | |
|---|---|---|
| 看的范围 | 只看前一个词 | 给定的长上下文 |
| 学的是什么 | 接续次数表 | 数值权重 |
| 嵌入与注意力 | 否 | 是 |
| 自己核实事实 | 否 | 否 |
FAI和参考文档
基于FAI项目,为浏览器Python重写示例。原FAI使用PyTorch、MPS及更大模型。环境文档核对日期:2026-10-18。
开发环境
确认浏览器内的Python环境。
v86运行32位Alpine,已安装Python。只用标准,无需pip、、账户或。
FAI用PyTorch处理与自动微分,MPS在支持的Mac上加速,不能用于此。Pyodide的普通监听与后台服务受限,所以全部必修练习在Alpine完成。
动手输入并实验
执行三条检查命令,查看Python版本、x86架构和ready。后续也使用此终端。
python3 --version
uname -m
python3 -c "import json, math, random, http.server; print('ready')"
检查理解
说明为何无需torch和MPS。训练在模拟的CPU中进行。
遇到问题时
不要安装torch,本课程不导入它。加载失败时检查网络并重试。
项目结构
创建目录、数据和设置。
源码放~/xsml,资料放data,权重放checkpoints。始终在项目目录执行。
每行一个Q:问题与A:答案。prepare.py去重后每五条留一条验证;验证资料不参与训练。使用有权使用的文字和虚构示例。
动手输入并实验
先建目录,再在nano输入下方各文件。保存数据和脚本后执行prepare.py,注意标点。
mkdir -p ~/xsml/data ~/xsml/checkpoints
cd ~/xsml
nano settings.json
nano data/raw.txt
nano prepare.py
python3 prepare.py
wc -l data/train.txt data/val.txt
settings.json — 需要亲手编写的完整源码
width是向量宽度,context是上下文长度,learning_rate是更新幅度。续训保持设置一致。
{
"width": 8,
"context": 12,
"merges": 12,
"steps": 20,
"learning_rate": 0.03,
"seed": 7,
"tokens": 24,
"max_tokens": 64,
"port": 8765
}
data/raw.txt — 需要亲手编写的完整源码
每行一个问答对。这些简短虚构英文可以换成自己的资料。
Q: hello | A: hello friend.
Q: hi | A: hi friend.
Q: name | A: i am xsml.
Q: sun | A: the sun is warm.
Q: cat | A: a cat can nap.
Q: dog | A: a dog can run.
Q: red | A: red is a color.
Q: blue | A: blue is a color.
Q: learn | A: i learn from text.
Q: code | A: i can learn code.
Q: moon | A: the moon is in the sky.
Q: rain | A: rain is water.
Q: book | A: i can read a book.
Q: tea | A: tea is warm.
Q: bye | A: bye friend.
prepare.py — 需要亲手编写的完整源码
读取UTF-8、去重、按完整记录划分训练和验证,避免同样例子同时出现。
from pathlib import Path
lines = [line.strip() for line in Path('data/raw.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
lines = list(dict.fromkeys(lines))
assert len(lines) >= 5, 'write at least five different Q/A lines'
assert all(' | A:' in line and line.startswith('Q:') for line in lines)
train = [line for i, line in enumerate(lines) if i % 5 != 0]
val = [line for i, line in enumerate(lines) if i % 5 == 0]
for name, rows in [('train', train), ('val', val)]:
text = '\n'.join(row.replace(' | A:', '\nA:') for row in rows) + '\n'
Path('data/' + name + '.txt').write_text(text, encoding='utf-8')
print(name, len(rows))
检查理解
用pwd与ls检查。示例分为12条训练、3条验证记录。
遇到问题时
文件未找到时检查cd ~/xsml及ls data。缩进用四空格。~/xsml-reference仅供对照,不自动填入项目。
BPE分词器
从自己的资料学习BPE词表。
从单字符开始,统计相邻对并合并最常见的一对。编码时按学习顺序重放规则。
不合并换行,未知字符用编号0。这是字符级教学版,不是完整级实现。词表和规则一起保存,训练后不能随意换词表。
动手输入并实验
输入并运行tokenizer.py,查看JSON。追踪merge中匹配时前进两位、否则一位的过程。
cd ~/xsml
nano tokenizer.py
python3 tokenizer.py
python3 -m json.tool data/tokenizer.json
tokenizer.py — 需要亲手编写的完整源码
merge合并一对;train学习有序规则;encode重放;decode拼接还原。
import json
from collections import Counter
from pathlib import Path
def merge(ids, pair, new_id):
result, i = [], 0
while i < len(ids):
if tuple(ids[i:i + 2]) == tuple(pair):
result.append(new_id)
i += 2
else:
result.append(ids[i])
i += 1
return result
class BPE:
def __init__(self, vocab, merges):
self.vocab, self.merges = vocab, merges
self.chars = {c: i for i, c in enumerate(vocab) if len(c) == 1}
@classmethod
def train(cls, text, count):
vocab = ['\ufffd'] + sorted(set(text) - {'\ufffd'})
ids = [vocab.index(c) for c in text]
merges = []
for _ in range(count):
pairs = Counter((a, b) for a, b in zip(ids, ids[1:])
if '\n' not in vocab[a] + vocab[b])
if not pairs:
break
pair, frequency = pairs.most_common(1)[0]
if frequency < 2:
break
new_id = len(vocab)
vocab.append(vocab[pair[0]] + vocab[pair[1]])
merges.append([list(pair), new_id])
ids = merge(ids, pair, new_id)
return cls(vocab, merges)
def encode(self, text):
ids = [self.chars.get(c, 0) for c in text]
for pair, new_id in self.merges:
ids = merge(ids, pair, new_id)
return ids
def decode(self, ids):
return ''.join(self.vocab[i] for i in ids)
def state(self):
return {'vocab': self.vocab, 'merges': self.merges}
if __name__ == '__main__':
settings = json.loads(Path('settings.json').read_text())
text = Path('data/train.txt').read_text(encoding='utf-8')
tokenizer = BPE.train(text, settings['merges'])
Path('data/tokenizer.json').write_text(
json.dumps(tokenizer.state(), ensure_ascii=False), encoding='utf-8')
assert tokenizer.decode(tokenizer.encode(text)) == text
print('vocab:', len(tokenizer.vocab), 'merges:', len(tokenizer.merges))
print('roundtrip: OK', tokenizer.encode('Q: hi\nA:'))
检查理解
roundtrip: OK说明原文完整复原。修改合并次数比较词元数量;未知字变为�。
遇到问题时
无重复对时零合并也正确。资料或词表改变后,应重新训练权重并备份旧模型。
GPT架构与代码
编写微型Transformer及梯度。
词元嵌入加位置;Query与Key计算关联,按混合Value。
只有一头一块,keys只含当前及过去位置,不能偷看未来。残差、归一化、两层MLP和输出头共同工作。为便于阅读,使用、无归一化,不使用dropout。
动手输入并实验
先写engine.py并检查导数,再写model.py。Value记录局部导数,backward逆序累加影响。
cd ~/xsml
nano engine.py
nano model.py
python3 engine.py
python3 -c "from model import GPT; m=GPT(10, 8, 12); print(len(m.forward([1, 2])), len(m.forward([1, 2])[0]))"
engine.py — 需要亲手编写的完整源码
data为数值,grad为梯度,parents保存局部导数。softmax把分数变概率。
import math
class Value:
def __init__(self, data, parents=()):
self.data = float(data)
self.grad = 0.0
self.parents = parents
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data + other.data, ((self, 1), (other, 1)))
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data,
((self, other.data), (other, self.data)))
def __pow__(self, power):
return Value(self.data ** power,
((self, power * self.data ** (power - 1)),))
def __neg__(self):
return self * -1
def __sub__(self, other):
return self + (-other)
def __truediv__(self, other):
return self * (other ** -1)
__radd__ = __add__
__rmul__ = __mul__
def exp(self):
result = math.exp(self.data)
return Value(result, ((self, result),))
def log(self):
return Value(math.log(self.data), ((self, 1 / self.data),))
def relu(self):
return Value(max(0, self.data), ((self, float(self.data > 0)),))
def backward(self):
order, seen, stack = [], set(), [(self, False)]
while stack:
node, expanded = stack.pop()
if expanded:
order.append(node)
elif node not in seen:
seen.add(node)
stack.append((node, True))
stack.extend((parent, False) for parent, _ in node.parents)
for node in order:
node.grad = 0.0
self.grad = 1.0
for node in reversed(order):
for parent, derivative in node.parents:
parent.grad += derivative * node.grad
def softmax(values):
peak = max(v.data for v in values)
exps = [(v - peak).exp() for v in values]
total = sum(exps)
return [v / total for v in exps]
def linear(x, weights):
return [sum(a * b for a, b in zip(x, row)) for row in weights]
def norm(x):
mean = sum(x) / len(x)
variance = sum((v - mean) ** 2 for v in x) / len(x)
return [(v - mean) * (variance + 1e-5) ** -0.5 for v in x]
if __name__ == '__main__':
x = Value(3)
y = x * x + 2 * x
y.backward()
print('y =', y.data, 'dy/dx =', x.grad)
assert y.data == 15 and x.grad == 8
model.py — 需要亲手编写的完整源码
先读shapes再读forward。全部权重参与学习,state与load保存数字。
import random
from engine import Value, linear, norm, softmax
class GPT:
def __init__(self, vocab_size, width, context, seed=7):
self.width, self.context = width, context
rng = random.Random(seed)
shapes = {
'token': (vocab_size, width), 'position': (context, width),
'query': (width, width), 'key': (width, width),
'value': (width, width), 'project': (width, width),
'expand': (2 * width, width), 'contract': (width, 2 * width),
'head': (vocab_size, width),
}
self.weights = {
name: [[Value(rng.gauss(0, 0.1)) for _ in range(cols)]
for _ in range(rows)] for name, (rows, cols) in shapes.items()
}
self.parameters = [v for matrix in self.weights.values()
for row in matrix for v in row]
def forward(self, ids):
assert 0 < len(ids) <= self.context
w, keys, values, output = self.weights, [], [], []
for position, token in enumerate(ids):
x = [a + b for a, b in zip(w['token'][token], w['position'][position])]
h = norm(x)
q = linear(h, w['query'])
keys.append(linear(h, w['key']))
values.append(linear(h, w['value']))
scores = [sum(a * b for a, b in zip(q, k)) / self.width ** 0.5
for k in keys]
attention = softmax(scores)
mixed = [sum(p * v[i] for p, v in zip(attention, values))
for i in range(self.width)]
x = [a + b for a, b in zip(x, linear(mixed, w['project']))]
hidden = [v.relu() for v in linear(norm(x), w['expand'])]
x = [a + b for a, b in zip(x, linear(hidden, w['contract']))]
output.append(linear(norm(x), w['head']))
return output
def loss(self, inputs, targets):
logits = self.forward(inputs)
losses = [-softmax(row)[target].log()
for row, target in zip(logits, targets)]
return sum(losses) / len(losses)
def state(self):
return {name: [[v.data for v in row] for row in matrix]
for name, matrix in self.weights.items()}
def load(self, state):
assert set(state) == set(self.weights), 'checkpoint keys'
for name, matrix in self.weights.items():
assert len(state[name]) == len(matrix), 'checkpoint rows'
for row, saved in zip(matrix, state[name]):
assert len(saved) == len(row), 'checkpoint columns'
for value, number in zip(row, saved):
value.data = float(number)
检查理解
导数检查得到15.0和8.0;形状检查得到2 10。解释为何未来词元不改变先前分数。
遇到问题时
检查矩阵与zip的维度。所有嵌入、注意力、MLP和输出权重都参与反向传播。
训练与检查点
训练、保存并继续。
输入[a,b]对应目标[b,c]。交叉熵是正确词元概率的负对数,低不保证事实正确。
选择窗口、清、前后向计算、限制梯度、SGD更新。每次完整更新保存权重、设置、词表、步数与随机状态。仅检查固定的首个验证窗口。
动手输入并实验
编写train.py,短训后再运行五步,查看resume和真实loss。速度因设备而异。
cd ~/xsml
nano train.py
python3 train.py --steps 20
python3 train.py --steps 5
train.py — 需要亲手编写的完整源码
资料哈希检查变更。临时文件加os.replace保留完整检查点;SGD没有动量状态。
import argparse
import hashlib
import json
import math
import os
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def main():
cfg = json.loads(Path('settings.json').read_text())
parser = argparse.ArgumentParser()
parser.add_argument('--steps', type=int, default=cfg['steps'])
args = parser.parse_args()
assert args.steps > 0, '--steps must be positive'
token_state = json.loads(Path('data/tokenizer.json').read_text())
tokenizer = BPE(**token_state)
train_text = Path('data/train.txt').read_text(encoding='utf-8')
val_text = Path('data/val.txt').read_text(encoding='utf-8')
fingerprint = hashlib.sha256((train_text + '\0' + val_text).encode()).hexdigest()
train, val = tokenizer.encode(train_text), tokenizer.encode(val_text)
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
rng, step = random.Random(cfg['seed']), 0
checkpoint = Path('checkpoints/model.json')
if checkpoint.exists():
saved = json.loads(checkpoint.read_text())
assert saved['settings'] == cfg and saved['tokenizer'] == token_state, 'changed settings/tokenizer: start a new checkpoint'
assert saved['data_hash'] == fingerprint, 'changed data: start a new checkpoint'
model.load(saved['weights'])
step = saved['step']
rng.setstate((saved['random'][0], tuple(saved['random'][1]), saved['random'][2]))
n = model.context
assert len(train) > n and len(val) > n, 'add more text'
print('parameters:', len(model.parameters), 'resume:', step, flush=True)
print('validation:', round(model.loss(val[:n], val[1:n + 1]).data, 4), flush=True)
checkpoint.parent.mkdir(exist_ok=True)
for _ in range(args.steps):
start = rng.randrange(len(train) - n)
window = train[start:start + n + 1]
for p in model.parameters:
p.grad = 0
loss = model.loss(window[:-1], window[1:])
loss.backward()
size = math.sqrt(sum(p.grad * p.grad for p in model.parameters))
scale = min(1.0, 1.0 / (size + 1e-8))
for p in model.parameters:
p.data -= cfg['learning_rate'] * p.grad * scale
step += 1
state = {'settings': cfg, 'tokenizer': token_state, 'data_hash': fingerprint,
'weights': model.state(), 'step': step, 'random': rng.getstate()}
temporary = checkpoint.with_suffix('.tmp')
temporary.write_text(json.dumps(state, ensure_ascii=False), encoding='utf-8')
os.replace(temporary, checkpoint)
print('step:', step, 'loss:', round(loss.data, 4), flush=True)
print('validation:', round(model.loss(val[:n], val[1:n + 1]).data, 4), flush=True)
print('saved:', checkpoint, flush=True)
if __name__ == '__main__':
main()
检查理解
确认model.json与连续步数。不同随机窗口的loss可能上升。
遇到问题时
Ctrl+C保留最近完成的更新。数据或设置变化时先导出并改名旧检查点,再重新训练。
文本生成
从分数生成文字。
每次输入最近并添加一个词元。为0选最高分,正数调整随机概率;top_k为0保留所有候选。
seed帮助重现实验。遇换行或长度上限停止,只返回新生成文字。保持训练时Q:/A:格式。
动手输入并实验
写generate.py,用同一检查点逐个改变温度、top_k和seed。
cd ~/xsml
nano generate.py
python3 generate.py --temperature 0 --top-k 1
python3 generate.py --temperature 0.8 --top-k 8 --seed 7
python3 generate.py --temperature 1.2 --top-k 0 --seed 8
generate.py — 需要亲手编写的完整源码
恢复结构和数字,截取上下文并反复采样。
import argparse
import json
import math
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def load():
saved = json.loads(Path('checkpoints/model.json').read_text())
cfg = saved['settings']
tokenizer = BPE(**saved['tokenizer'])
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
model.load(saved['weights'])
return model, tokenizer, cfg
def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
raise ValueError('invalid sampling parameters')
ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
for _ in range(tokens):
logits = model.forward(ids[-model.context:])[-1]
ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
if temperature == 0:
token = ranked[0]
else:
candidates = ranked[:top_k] if top_k else ranked
peak = logits[candidates[0]].data
weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
token = rng.choices(candidates, weights=weights)[0]
ids.append(token)
if tokenizer.vocab[token] == '\n':
break
result.append(token)
return tokenizer.decode(result)
if __name__ == '__main__':
model, tokenizer, cfg = load()
parser = argparse.ArgumentParser()
parser.add_argument('--prompt', default='Q: hi\nA:')
parser.add_argument('--tokens', type=int, default=cfg['tokens'])
parser.add_argument('--temperature', type=float, default=0.8)
parser.add_argument('--top-k', type=int, default=8)
parser.add_argument('--seed', type=int, default=cfg['seed'])
args = parser.parse_args()
assert 1 <= args.tokens <= cfg['max_tokens']
print(generate(model, tokenizer, args.prompt, args.tokens,
args.temperature, args.top_k, args.seed))
检查理解
相同输入、权重和设置应复现。首个词元为换行时可能得到空答案。
遇到问题时
小模型短训后乱码正常。先检查格式、未知字和损失,增加随机性不能替代训练。
Runner与API
建立真正的HTTP Runner。
Runner只加载一次模型,顺序处理请求。GET /health返回状态,POST /generate返回生成文本。
127.0.0.1指来宾Linux自身。必须在此终端运行,不是在浏览器地址栏访问。nohup与&只在此标签存活时运行,刷新后需重启,不需systemd。
动手输入并实验
编写runner.py并启动,立即记录PID,检查日志、health和生成结果。
cd ~/xsml
nano runner.py
nohup python3 -u runner.py > runner.log 2>&1 &
echo $! > runner.pid
cat runner.log
curl -s http://127.0.0.1:8765/health
curl -s http://127.0.0.1:8765/generate -H 'Content-Type: application/json' -d '{"prompt":"Q: hi
A:","tokens":8,"temperature":0.8}'
runner.py — 需要亲手编写的完整源码
共享一个模型,检查输入并返回UTF-8 JSON,仅监听来宾回环地址。
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from generate import load, generate
def main():
model, tokenizer, cfg = load()
class Handler(BaseHTTPRequestHandler):
def reply(self, status, body):
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path == '/health':
self.reply(200, {'status': 'ok', 'model': 'XSML'})
else:
self.reply(404, {'error': 'not found'})
def do_POST(self):
if self.path != '/generate':
return self.reply(404, {'error': 'not found'})
try:
length = int(self.headers.get('Content-Length', '0'))
if not 0 < length <= 4096:
raise ValueError('request size')
data = json.loads(self.rfile.read(length))
prompt = data['prompt']
tokens = data.get('tokens', cfg['tokens'])
temperature = data.get('temperature', 0.8)
top_k = data.get('top_k', 8)
seed = data.get('seed', cfg['seed'])
if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
raise ValueError('prompt')
if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
raise ValueError('tokens')
if type(top_k) is not int or top_k < 0 or type(seed) is not int:
raise ValueError('top_k/seed')
if type(temperature) not in (int, float):
raise ValueError('temperature')
result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
self.reply(200, {'text': result, 'model': 'XSML'})
except (ValueError, KeyError, TypeError):
self.reply(400, {'error': 'invalid request'})
address = ('127.0.0.1', cfg['port'])
print('XSML Runner:', address, flush=True)
HTTPServer(address, Handler).serve_forever()
if __name__ == '__main__':
main()
检查理解
应返回status ok及含text的JSON。空JSON应返回400而非崩溃。
遇到问题时
拒绝连接时检查日志和检查点。端口占用时先停旧PID。重新训练后重启Runner。
创建XChat
完成自己的聊天客户端。
XChat是界面,Runner是执行服务,XSML是模型。给问题加格式,通过获取回答。
初版在浏览器终端运行,每题独立,不发送历史。下一步可在小上下文限制内增加历史。重点是请求真正经过自己训练的模型。
动手输入并实验
保持Runner运行,编写并运行xchat.py,在You>输入hi。先/exit回到bash,再执行kill。
cd ~/xsml
nano 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 -> XSML | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSML>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
检查理解
展示XChat→Runner→权重→生成文本。停Runner应报连接错误,重启后恢复。
遇到问题时
不要在XChat里输入kill,它会成为问题。先退出,完成后导出项目。