本地推理与模型服务:先看清 HTTP 契约,再决定是否自己部署
正在读取学习记录…
模型推理服务接收请求,运行模型,并返回候选回答;Agent 程序负责组装请求、接收结果和决定怎样使用。两者通过接口通信,模型服务可以由外部供应商提供,也可以部署在本机或组织服务器。
先启动一个固定响应的本机 HTTP 服务,看清 Agent 发出的请求和收到的响应。走通接口后,再比较外部 API、本机和内网部署的成本、运维与数据边界。
学习分两步:先用固定响应服务走通 HTTP 接口,再选择引擎加载真实权重。随文程序完成第一步,后面的迁移路线接到第二步;真实模型的计算将由推理引擎完成。
一、先画出 Agent 与模型服务的边界
例如,客户说“付款成功,但订单没有记录”,客服程序把这段话和“推荐处理部门”的要求发送给模型服务。服务返回“建议账单组处理”,客服程序检查部门名称和推荐理由,再展示给客服人员。HTTP 200 是本次请求成功收到响应的状态码;推荐是否合适,可以对照客户描述和部门职责检查。资料不足时,回答中列出需要补充的信息。
可以把推理服务想成厨房:模型权重提供厨师已经学到的做法,推理引擎负责实际计算,HTTP服务是接单和出餐的窗口。请求的 model 指定用哪个模型,messages 是点单内容,响应 choices 装候选回答。窗口运转正常和菜是否合口味,要分别检查。
| 概念 | 它是什么 | 初学者容易误以为 |
|---|---|---|
| 模型权重 | 训练后的参数文件 | 下载了权重就有稳定线上服务 |
| 推理引擎 | 加载权重并计算下一个 token 的程序 | 它会替业务判断真实性和权限 |
| 模型服务 | 用 HTTP 等接口包装推理引擎 | API 兼容就意味着全部功能、错误语义相同 |
| Agent 客户端 | 组装请求、解析结果并调用其他组件 | 模型一句“完成”就能视为执行成功 |
官方 llama.cpp 的服务文档展示了本机 HTTP 服务与兼容接口;vLLM 的服务文档也提供 HTTP 入口。两者的启动参数和支持字段各有差异,按所选引擎的文档配置。
二、亲手跑固定响应的服务接口
复制下方完整代码,保存为 service_contract.py、test_service_contract.py ,放到同一文件夹。在终端中进入该目录,运行:
完整代码:service_contract.py
"""启动本机接口,用固定响应练习请求发送与回答检查。"""
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def inspect_response(payload):
"""从choices列表中取出回答,检查message及正文的格式。"""
if not isinstance(payload, dict):
return "invalid_response"
choices = payload.get("choices")
if not isinstance(choices, list) or not choices:
return "invalid_response"
first = choices[0]
if not isinstance(first, dict) or not isinstance(first.get("message"), dict):
return "invalid_response"
content = first["message"].get("content")
if not isinstance(content, str) or not content.strip():
return "invalid_response"
return "candidate_only"
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args):
pass # Avoid echoing potentially sensitive request data in this fixture.
def send_json(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_POST(self):
if self.path != "/v1/chat/completions":
self.send_json(404, {"error": "unknown_path"})
return
try:
length = int(self.headers.get("Content-Length", "0"))
if length < 1 or length > 16384:
raise ValueError("request_size")
body = json.loads(self.rfile.read(length).decode("utf-8"))
except (ValueError, UnicodeError, json.JSONDecodeError):
self.send_json(400, {"error": "invalid_json_or_size"})
return
if not isinstance(body, dict) or body.get("model") != "fixture-model":
self.send_json(404, {"error": "model_not_found"})
return
messages = body.get("messages")
if not isinstance(messages, list) or not messages:
self.send_json(400, {"error": "messages_required"})
return
if any(not isinstance(m, dict) or m.get("role") not in ("user", "system")
or not isinstance(m.get("content"), str) for m in messages):
self.send_json(400, {"error": "invalid_message"})
return
self.send_json(200, {
"model": "fixture-model",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "需要人工核实"},
"finish_reason": "stop"}],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
"fixture": True,
})
if __name__ == "__main__":
server = ThreadingHTTPServer(("127.0.0.1", 8765), Handler)
print("offline fixture listening at http://127.0.0.1:8765")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()完整代码:test_service_contract.py
# 核对本地推理与模型服务的正常结果和错误分支;断言失败时会显示对应的测试。
import json
import threading
import unittest
from http.server import ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from service_contract import Handler, inspect_response
class ContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
cls.url = "http://127.0.0.1:%d/v1/chat/completions" % cls.server.server_port
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
cls.server.server_close()
cls.thread.join(timeout=3)
def post(self, body):
data = json.dumps(body).encode("utf-8")
request = Request(self.url, data=data, headers={"Content-Type": "application/json"})
with urlopen(request, timeout=2) as response:
return response.status, json.load(response)
def test_valid_response_is_only_a_candidate(self):
status, body = self.post({"model": "fixture-model", "messages": [{"role": "user", "content": "hi"}]})
self.assertEqual(status, 200)
self.assertEqual(inspect_response(body), "candidate_only")
def test_missing_messages_rejected(self):
with self.assertRaises(HTTPError) as error:
self.post({"model": "fixture-model", "messages": []})
self.assertEqual(error.exception.code, 400)
def test_wrong_model_rejected(self):
with self.assertRaises(HTTPError) as error:
self.post({"model": "other", "messages": [{"role": "user", "content": "hi"}]})
self.assertEqual(error.exception.code, 404)
def test_missing_choices_not_accepted(self):
self.assertEqual(inspect_response({"choices": []}), "invalid_response")
def test_empty_content_not_accepted(self):
self.assertEqual(inspect_response({"choices": [{"message": {"content": ""}}]}), "invalid_response")
if __name__ == "__main__":
unittest.main()python -m unittest -v test_service_contract.py末尾应显示 Ran 5 tests 和 OK。测试完成后,在同一个窗口启动服务:
python service_contract.py启动后显示:
offline fixture listening at http://127.0.0.1:8765这个程序启动后会持续等待请求。保持它打开,再打开第二个终端发送请求。Windows 使用下面的 PowerShell 命令,macOS 使用紧接着的 curl 命令:
$body = @{ model='fixture-model'; messages=@(@{role='user'; content='你好'}) } | ConvertTo-Json -Depth 5
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:8765/v1/chat/completions' -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body))macOS:使用 curl 发送请求
curl -sS -w '\nHTTP %{http_code}\n' \
-H 'Content-Type: application/json' \
-d '{"model":"fixture-model","messages":[{"role":"user","content":"你好"}]}' \
http://127.0.0.1:8765/v1/chat/completions应看到固定的“需要人工核实”和 fixture: true,用量零值是占位符。中文请求先编码为 UTF-8 字节,服务端按同一编码读取。保持第一个窗口打开,继续下面的错误请求;全部完成后按 Ctrl+C 结束。端口被占用时,找到 service_contract.py 中的 ThreadingHTTPServer(("127.0.0.1", 8765), Handler),把其中的端口和下方所有请求地址里的端口一起改为 8766,再启动服务;实验后恢复 8765。
为什么先做这个离线练习
真实模型服务出错时,先判断故障在请求、网络、模型加载、生成、解析还是业务校验。固定响应服务把生成结果保持不变,便于单独检查请求、响应与错误分类。
现在故意把请求里的 model 改成 other,观察 HTTP 404;再把 messages 改成空数组,观察 HTTP 400。测试文件还会检查:响应缺少 choices、消息内容为空时,客户端不得把它标成可用结果。正常、故障、边界三类证据就这样分开记录。想读源码时,先看 Handler.do_POST 的路径、大小和字段检查,再看 inspect_response:它即使接收到格式正确的响应,也只返回 candidate_only,没有业务授权。
初次使用 PowerShell 时要注意:Invoke-RestMethod 遇到 400/404 会报错,不会像成功响应一样打印一个普通对象。若只看到红字,不是服务突然崩溃。保持第一个窗口的服务运行,在第二个窗口使用下面的三组请求;catch 只用于显示 HTTP 状态,不把错误改写成成功。
# 在保存文件的目录执行,先核对文件,再运行程序和测试。
$uri = 'http://127.0.0.1:8765/v1/chat/completions'
$requests = @(
@{ name='正常'; model='fixture-model'; messages=@(@{role='user'; content='hello'}) },
@{ name='错误模型'; model='other'; messages=@(@{role='user'; content='hello'}) },
@{ name='空消息'; model='fixture-model'; messages=@() }
)
foreach ($item in $requests) {
$json = @{model=$item.model; messages=$item.messages} | ConvertTo-Json -Depth 5
try {
$result = Invoke-WebRequest -Method Post -Uri $uri -ContentType 'application/json' -Body ([Text.Encoding]::UTF8.GetBytes($json))
"$($item.name): HTTP $($result.StatusCode)"
} catch {
"$($item.name): HTTP $([int]$_.Exception.Response.StatusCode)"
}
}macOS:使用 curl 发送请求
# 正常请求:预期 HTTP 200
curl -sS -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d '{"model":"fixture-model","messages":[{"role":"user","content":"hello"}]}' http://127.0.0.1:8765/v1/chat/completions
# 模型名称错误:预期 HTTP 404
curl -sS -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d '{"model":"other","messages":[{"role":"user","content":"hello"}]}' http://127.0.0.1:8765/v1/chat/completions
# 消息为空:预期 HTTP 400
curl -sS -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d '{"model":"fixture-model","messages":[]}' http://127.0.0.1:8765/v1/chat/completions预期正常200、错误模型404、空消息400,这是随文服务定义的错误码;其他接口按各自契约处理。若三条都连接失败,检查第一个窗口是否仍监听 127.0.0.1:8765。错误路径是另一种输入:保持正常请求体,只把 URL 尾部换成 /v1/missing:
try {
Invoke-WebRequest -Method Post -Uri 'http://127.0.0.1:8765/v1/missing' -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body))
} catch {
"错误路径: HTTP $([int]$_.Exception.Response.StatusCode)"
}macOS:使用 curl 发送请求
curl -sS -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d '{"model":"fixture-model","messages":[{"role":"user","content":"hello"}]}' http://127.0.0.1:8765/v1/missing应得到404,源码返回 unknown_path;错误 model 的404返回 model_not_found。相同状态码可以有不同原因,要结合响应错误字段定位。
三、外部 API、本机和私有服务怎么选
接口走通后,再问模型在哪里计算。如果你要实际在本机加载模型,先确认内存、显存和许可证;参数量、量化方式与上下文长度都会影响资源需求,实际加载记录比仅看参数量有用。
| 需要回答的问题 | 应收集的证据 | 常见误判 |
|---|---|---|
| 数据能否出域? | 数据分类、供应商处理政策、内网要求 | “本地”二字等于合规 |
| 电脑能否运行? | 模型文件大小、内存/显存余量、实际加载记录 | 参数量直接等于占用 |
| 多人同时用怎么办? | 并发、排队时间、超时、失败率 | 单人演示流畅就能上线 |
| 质量够不够? | 同一测试集、人工标注、各类错误 | 能回答一个例子就够 |
| 服务谁来维护? | 版本、日志、监控、备份、回滚、权限 | 有 HTTP 端口就是生产服务 |
假如你只是一个人在电脑上验证学习思路,可以先用本机服务;有敏感工单和多用户并发需求时,先做数据边界、硬件和运维评估;如果政策允许外部 API,也要把供应商价格、限流、模型更新与隐私条款列进清单。不要在没有基准数据时宣布某一种方案一定更快、更便宜或更安全。
四、从接口练习接到真实模型
- 选一个许可证与用途匹配的模型,记录模型名、权重版本和来源。模型服务项目的开源许可证不自动覆盖模型权重许可证。
- 按所选服务项目的官方文档加载模型,先在本机只监听回环地址;对外提供服务前,先配置身份验证和访问范围。项目接口与默认安全设置会变化,上线前逐项核对。
- 用相同的固定测试集,对真实服务记录请求、响应、结束原因、延迟、错误类型和资源占用。与教学替身的“5 项通过”分开存档。
- 保持 L1 前面建立的解析、Schema、业务事实与人工交接;换模型或部署地点后,沿用这些检查并重新运行样例。
- 再做限流、超时、并发、监控和故障回退。含写入或执行工具的 Agent,要另外做授权与幂等设计。
读 llama.cpp 服务 README 时,先找启动命令、监听地址和 API 路径;再与 vLLM 服务文档的接口、鉴权和部署要求对照。选型时核对具体版本与维护状态。
可带走的结论
运行正常、错误模型、空消息与错误路径四组请求,解释两个404为什么不同。再指出固定响应“需要人工核实”在哪段代码生成;真正推理时,这里会换成引擎根据权重和消息计算的候选。选型再用相同任务集比较质量、容量、成本和数据责任。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“本地推理与模型服务”,哪种做法更稳妥?
对应知识点:L1-inference
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L1-inference
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L1-inference
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L1-inference
服务部署与接口配置可查阅 llama.cpp 服务文档和 vLLM 在线服务文档。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。