SONUB
ZH English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

如何开启通知

此浏览器或网址无法使用通知。请使用支持通知的浏览器,通过安全连接访问SONUB。

如果通知被屏蔽,请在浏览器设置中允许通知。

  1. 打开地址栏旁的站点设置或浏览器的站点设置,将此网站的通知设为允许。
  2. 在设备设置中允许浏览器发送通知,并检查勿扰模式或专注模式。
  3. 返回SONUB并刷新页面。通知开启后,铃铛将不再显示。

在iPhone或iPad上,将SONUB添加到主屏幕,从主屏幕打开后允许通知。

登录 退出登录 显示主题 默认主题 深色主题 粉色主题 聊天 1:1 聊天 群聊 论坛 公告 新闻区 建议区 自我介绍 主菜单

现在,让我们打造属于你自己的人工智能吧。

通过真正的 提供好的。写好 runner.py 并在后台启动,再用 调用 和 /generate。

一排词语积木的最后是一个空格。空格上方有三块候选积木,各带一根高低不同的柱子;其中一块被选中,接到这一排的末尾,一个箭头又绕回去,再选下一个。
8

Runner与API

只加载一次,顺序处理请求。 返回状态, /generate返回生成文本。

指自身。必须在此运行,不是在地址栏访问。与&只在此标签存活时运行,刷新后需重启,不需。

为什么做成服务器——读一次,用多次

用运行 generate.py,每次运行都要读取检查点、重建模型。问题多时,每次都重复同样的准备。Runner 只读一次模型放在里,每次请求来时只 generate。

拆成,角色也分开了。

  • Runner(服务器):持有模型,只负责计算。
  • ():只负责和人交流的界面。
  • 两者之间的约定():向哪个地址发送什么 ,会返回什么 JSON。

只要遵守约定,客户端可以是任何东西。网页、手机应用、其他服务器都可以代替终端 XChat 调用同一个 Runner。这种读取好的来负责计算的也叫 。Runner 是很小的运行时,实际中 、、 这样的运行时能快速完成同样的工作。

HTTP 基础——请求与响应的形状

是客户端发送请求、服务器返回响应的约定。

  • 请求:方法(GET 是获取,POST 是发送)、(/health, /generate)、头部(像 : application/json 这样说明正文类型)、正文(POST 的 JSON)。
  • 响应:、头部、正文。
  • 状态码: 成功,400 错误请求(客户端的问题), 路径不存在,500 服务器错误(服务器的问题),503 还没准备好。

可以用 curl 直接构造请求。

  • curl -s 地址:发送 GET 请求,安静地只显示正文。
  • -H 'Content-Type: application/json':添加头部。
  • -d '{…}':添加正文。有正文时 curl 用 POST 发送。
  • -i:连响应的状态行和头部也显示。输入 curl -i http://127.0.0.1:8765/health 试试。会看到 HTTP/1.0 200 OK、Content-Type、Content-Length 这些行。

Runner 的 API——地址、字段、限制

Runner 只开放两个地址。

  • GET /health:确认是否在运行。响应是 {"status": "ok", "model": ""}。
  • POST /generate:生成文字。响应是 {"text": "生成的文字", "model": "XSLM"}。
  • 其他地址:404 和 {"error": "not found"}。

/generate 正文的字段。

  • (必填):字符串,1~512 个字符。
  • :整数,1~64(max_tokens)。没有就是 24。
  • :0 以上的数字。没有就是 0.8。
  • :0 以上的整数。没有就是 8。
  • seed:整数。没有就是 7。

对错误的请求,服务器不会崩溃,而是用 400 和 {"error": "invalid request"} 回答。正文超过 4,096 、不是 JSON、字段格式错误、没有 prompt,全部是 400。设限制是为了保护服务器。一个把 tokens 设为 100 万的请求,就能把服务器占住几个小时。公开服务器必须做这样的检查。

127.0.0.1 与端口——谁能连接

Runner 在 127.0.0.1 的 8765 号等待请求。

  • 127.0.0.1():回环地址,意思是"这台计算机自己"。只有同一台计算机内的程序能连接。
  • 端口:在一台计算机上区分多个服务器的编号。8765 是 settings.json 的 port。
  • 0.0.0.0:表示从所有接收。要让其他计算机连接时就这样开放,但不加认证开放到互联网,任何人都能使用你的服务器,要小心。

这里的"这台计算机"是浏览器中的虚拟 。所以 curl 和 XChat 必须在右边的终端里运行。在自己电脑的浏览器地址栏输入 localhost:8765,指向的是自己的电脑而不是虚拟 Linux,连接不上。

两个程序不能同时使用同一个端口。Runner 已经启动又再启动,会出现 OSError: 。

后台运行——&、nohup、PID、kill

Runner 是不会结束、一直等待请求的程序。直接运行会占住终端,无法输入其他命令,所以在后面(后台)运行。

  • &:加在命令末尾,就在后台运行并立即返回提示符。
  • nohup:终端关闭了程序也不结束。
  • -u:输出不攒着,立即写出。不会。
  • > runner.log :把普通输出和错误输出都写入 runner.log 文件。
  • $!:刚在后台启动的程序的编号(PID)。用 $! > runner.pid 保存。
  • "$(cat runner.pid)":向保存的编号的程序发送结束信号。
  • ps 或 jobs:查看当前运行的程序。

真实服务器在计算机重启、程序崩溃时也要自动重新启动。Linux 用 systemd 服务(Restart=always), 用 launchd 做这件事。这个虚拟 Linux 中的 Runner 只在这个标签页打开时运行。

并发请求——逐个处理与排队

XSLM 的 一次处理一个请求。生成结束前进来的请求,要等前一个请求结束。对教学来说简单又安全。

多人使用的服务器做法不同。

  • ThreadingHTTPServer:每个请求单独建线程,同时接收。里就有。
  • FastAPI + uvicorn:用(async)接收大量连接,实际生成用 Semaphore(1)一次一个排队。因为 内存只有一份,同时跑多个生成可能撑爆内存。
  • 批处理:把短时间(例如 100ms)内进来的请求打包一次计算,能更高效地使用 GPU。这是大型服务的核心技术。

FastAPI 会自动生成请求格式检查(pydantic)和 /docs 的 API 文档(Swagger)。XSLM 在 runner.py 中手写同样的检查。

走向真实服务——速度、规模、安全

要把 Runner 做成真实服务,还要加上这些。

  • 快速运行时:用 llama.cpp(擅长 和苹果芯片)、ONNX Runtime(多种设备)、vLLM(GPU 服务器)这类优化过计算的引擎来运行权重。
  • :把权重从 32、16 位改存为 8、4 位整数,节省内存和时间。8 位用 256 个等级表示数字,用 整数 × 缩放值 还原成接近原来的数字。会稍微下降。
  • :保存前面词元的 key、value,不必每个词元都重新计算整个。
  • 输出:词元一生成就立即发送,用户不用等。
  • 安全:认证()、请求次数限制、输入大小限制、日志与监控、HTTPS。
  • 内存计算:运行内存 = 权重 + KV 缓存 + 。 模型即使压到 4 位,实际也要 5~8GB。

动手输入并实验

编写runner.py并启动,立即记录PID,检查日志、health和生成结果。

cd ~/xslm
micro 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\nA: ","tokens":8,"temperature":0.8}'
在终端中练习
runner.py需要亲手编写的完整源码

共享一个模型,检查输入并返回 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': 'XSLM'})
            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': 'XSLM'})
            except (ValueError, KeyError, TypeError):
                self.reply(400, {'error': 'invalid request'})

    address = ('127.0.0.1', cfg['port'])
    print('XSLM Runner:', address, flush=True)
    HTTPServer(address, Handler).serve_forever()


if __name__ == '__main__':
    main()

逐行拆解代码

runner.py 只读一次模型

def main():
    model, tokenizer, cfg = load()

main 启动时用 load() 读一次模型、和设置。下面的 类定义在 main 内部,直接使用外面的 model、tokenizer、cfg(闭包)。不会每次请求都重建模型。

runner.py reply——发送响应

    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)

Handler 继承 BaseHTTPRequestHandler,负责一个请求。reply 把转成 JSON,再变成 UTF-8 字节(因为 ensure_ascii=False,中文也原样保留),按 状态码 → 头部(Content-Type、Content-Length) → 空行(end_headers) → 正文 的顺序发送。Content-Length 不是字符数而是字节数,所以用编码后 的长度。

runner.py GET /health

        def do_GET(self):
            if self.path == '/health':
                self.reply(200, {'status': 'ok', 'model': 'XSLM'})
            else:
                self.reply(404, {'error': 'not found'})

收到 GET 请求时调用 do_GET。self.path 是 /health 就发送 200 和状态,否则发送 404。状态检查地址不做模型计算,直接回答,所以用来轻量地确认服务器是否在运行。

runner.py POST /generate——读取并检查正文

        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')

路径不是 /generate 就是 404。然后在 try 中逐项检查,只要有一项不对就抛出 。

  • 用 Content-Length 头部得知正文长度,不在 1~4,096 字节之间就拒绝。只从 rfile 读取这个长度。
  • 用 json.loads 把正文变成字典。不是 JSON 就是 ValueError。
  • prompt 必须有(没有就是 ),其余用 get 给出默认值。
  • 用 type(x) is not int 检查,是因为 isinstance(True, int) 为真。这样可以挡住把 true 当作词元数发送的请求。
  • temperature 整数和浮点数都接受,负数、 由 generate 再次拒绝。

runner.py 生成并回答

                result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
                self.reply(200, {'text': result, 'model': 'XSLM'})
            except (ValueError, KeyError, TypeError):
                self.reply(400, {'error': 'invalid request'})

通过检查就调用 generate,用 200 和 {"text": …} 回答。ValueError、KeyError、TypeError 都是错误请求,所以用 400 回答,服务器继续运行。如果每次出错服务器都崩溃,一个请求就能让整个服务停下。

runner.py 在 127.0.0.1 上等待

    address = ('127.0.0.1', cfg['port'])
    print('XSLM Runner:', address, flush=True)
    HTTPServer(address, Handler).serve_forever()

地址是 127.0.0.1 和设置中的端口。用 =True 立即输出启动信息,可以在 runner.log 中知道是否已启动。HTTPServer(…).serve_forever() 无休止地重复逐个接收请求并交给 Handler。用 kill 或 停止。

检查理解

应返回status ok及含text的JSON。空JSON应返回400而非崩溃。

遇到问题时

时检查日志和检查点。端口占用时先停旧PID。重新训练后重启Runner。

常见问题

为什么在浏览器地址栏打开 localhost:8765/health 不行?

Runner 在浏览器中的虚拟 Linux 里运行,只在那个 Linux 的 127.0.0.1 上等待。地址栏的 localhost 是你真正的电脑,那里没有 Runner。请在右边的终端运行 curl。

出现 Address already in use。

8765 号端口已经被另一个 Runner 占用了。可以直接用已启动的 Runner,或者用 kill "$(cat runner.pid)" 关掉后重新启动。没有 PID 文件时,用 ps 找到 python3 runner.py 的编号再 kill。

多训练了,但 Runner 的回答没变。

Runner 只在启动时读一次 checkpoints/model.json 放在内存里。文件变了它也不知道。要关掉再启动才会用新权重。真实服务会在旁边启动新模型,再把请求转过去,不停机地切换。

学习指南

人工智能

智能体工具

Vibe 编程

AI 应用

AI证书

AI课堂

IT 基础

聊天

论坛

创作

社区

更多

术语词典

选一个你不认识的词吧,可以看到简单的解释和相关术语。

还没有匹配的术语。

选择一个术语即可查看解释。