AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

本地推理与模型服务:先看清 HTTP 契约,再决定是否自己部署

我的学习 →

正在读取学习记录…

模型推理服务接收请求,运行模型,并返回候选回答;Agent 程序负责组装请求、接收结果和决定怎样使用。两者通过接口通信,模型服务可以由外部供应商提供,也可以部署在本机或组织服务器。

先启动一个固定响应的本机 HTTP 服务,看清 Agent 发出的请求和收到的响应。走通接口后,再比较外部 API、本机和内网部署的成本、运维与数据边界。

学习分两步:先用固定响应服务走通 HTTP 接口,再选择引擎加载真实权重。随文程序完成第一步,后面的迁移路线接到第二步;真实模型的计算将由推理引擎完成。

一、先画出 Agent 与模型服务的边界

Agent 客户端通过 HTTP 请求模型服务,模型返回候选文本后,客户端仍需解析、事实和授权校验
图 1:服务返回候选文本,业务系统再解析、检查并决定是否使用。

例如,客户说“付款成功,但订单没有记录”,客服程序把这段话和“推荐处理部门”的要求发送给模型服务。服务返回“建议账单组处理”,客服程序检查部门名称和推荐理由,再展示给客服人员。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
Python
"""启动本机接口,用固定响应练习请求发送与回答检查。"""

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
Python
# 核对本地推理与模型服务的正常结果和错误分支;断言失败时会显示对应的测试。
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()
Bash
python -m unittest -v test_service_contract.py

末尾应显示 Ran 5 tests 和 OK。测试完成后,在同一个窗口启动服务:

Bash
python service_contract.py

启动后显示:

TXT
offline fixture listening at http://127.0.0.1:8765

这个程序启动后会持续等待请求。保持它打开,再打开第二个终端发送请求。Windows 使用下面的 PowerShell 命令,macOS 使用紧接着的 curl 命令:

PowerShell
$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 发送请求
Bash
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 状态,不把错误改写成成功。

PowerShell
# 在保存文件的目录执行,先核对文件,再运行程序和测试。
$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 发送请求
Bash
# 正常请求:预期 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:

PowerShell
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 发送请求
Bash
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、本机和私有服务怎么选

接口走通后,再问模型在哪里计算。如果你要实际在本机加载模型,先确认内存、显存和许可证;参数量、量化方式与上下文长度都会影响资源需求,实际加载记录比仅看参数量有用。

外部 API、本机运行和私有服务,在数据边界、硬件、稳定性、质量与成本上的证据矩阵
图2:从数据边界、硬件、稳定性、质量和成本比较三种部署方式;选型时填入自己的测量数据。
需要回答的问题应收集的证据常见误判
数据能否出域?数据分类、供应商处理政策、内网要求“本地”二字等于合规
电脑能否运行?模型文件大小、内存/显存余量、实际加载记录参数量直接等于占用
多人同时用怎么办?并发、排队时间、超时、失败率单人演示流畅就能上线
质量够不够?同一测试集、人工标注、各类错误能回答一个例子就够
服务谁来维护?版本、日志、监控、备份、回滚、权限有 HTTP 端口就是生产服务

假如你只是一个人在电脑上验证学习思路,可以先用本机服务;有敏感工单和多用户并发需求时,先做数据边界、硬件和运维评估;如果政策允许外部 API,也要把供应商价格、限流、模型更新与隐私条款列进清单。不要在没有基准数据时宣布某一种方案一定更快、更便宜或更安全。

四、从接口练习接到真实模型

  1. 选一个许可证与用途匹配的模型,记录模型名、权重版本和来源。模型服务项目的开源许可证不自动覆盖模型权重许可证。
  2. 按所选服务项目的官方文档加载模型,先在本机只监听回环地址;对外提供服务前,先配置身份验证和访问范围。项目接口与默认安全设置会变化,上线前逐项核对。
  3. 用相同的固定测试集,对真实服务记录请求、响应、结束原因、延迟、错误类型和资源占用。与教学替身的“5 项通过”分开存档。
  4. 保持 L1 前面建立的解析、Schema、业务事实与人工交接;换模型或部署地点后,沿用这些检查并重新运行样例。
  5. 再做限流、超时、并发、监控和故障回退。含写入或执行工具的 Agent,要另外做授权与幂等设计。

读 llama.cpp 服务 README 时,先找启动命令、监听地址和 API 路径;再与 vLLM 服务文档的接口、鉴权和部署要求对照。选型时核对具体版本与维护状态。

可带走的结论

运行正常、错误模型、空消息与错误路径四组请求,解释两个404为什么不同。再指出固定响应“需要人工核实”在哪段代码生成;真正推理时,这里会换成引擎根据权重和消息计算的候选。选型再用相同任务集比较质量、容量、成本和数据责任。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“本地推理与模型服务”,哪种做法更稳妥?

对应知识点:L1-inference

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L1-inference

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L1-inference

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L1-inference

服务部署与接口配置可查阅 llama.cpp 服务文档和 vLLM 在线服务文档。

图片查看器
100%

文章配图 · 按 Esc 关闭