L1 总览:把模型回答变成可检查的接口
正在读取学习记录…
L1 学的是程序怎样与模型交换信息:先组成请求,再接收回答,按约定检查,最后决定是否使用。模型回答是一份候选建议;真正执行写入的是业务程序。L0 已让 Python、HTTP 和任务边界有了基础,这一层把它们接到模型接口上。
先看一个客户求助:“付款成功,但订单页还是空的。”客服系统把这条求助保存成一条记录,称为“工单”。模型读完后推荐账单组(代码中记作 Billing),并说明理由:“付款与订单记录不一致,需要核对账单。”程序检查推荐部门和理由,再把建议展示给客服人员,由客服人员决定交给谁处理。后面逐步讨论信息不足时如何追问、请求超时后如何处理。
L1 沿用这个客服例子,学习怎样把任务和客户留言发给模型、怎样检查它返回的部门与理由,以及怎样记录调用失败。这些步骤也能用在邮件分类、资料整理等任务中。
十二项知识点怎样接起来
一直带着上面那条“付款成功却看不到订单”的工单读。每读完一段,就问自己一个新问题:
| 读到哪里 | 此时要解决的问题 | 手里应该多出什么 |
|---|---|---|
| 提出任务:消息、Prompt、示例与采样 | 谁给要求?换示例或参数,回答会怎样变? | 带来源、可比较的请求 |
| 结构化输出与约束 | 这段回答能安全变成程序参数吗? | 解析、字段和业务三道检查 |
| 重试与流式 | 超时了、只收到半句时,该停还是继续? | 有上限、有未知状态的处理规则 |
| 比较选择:路由、版本、优化与本地服务 | 发给哪个模型?改版到底更好了吗? | 数据边界、对比证据与服务契约 |
这张表是阅读时的“工单进度条”:按四段路线前进,每次掌握一个动作,再通过对应程序观察结果。
第一段先让请求可比较:消息角色与指令优先级确认哪段文字有权提出要求,任务、约束与 Prompt 编写把模糊需求改写成可验收任务,Zero-shot 与 Few-shot只改变是否给示例,采样参数与输出波动让比较时保留模型、参数、样例和多次输出。交付物不是“最强提示词”,而是一份可版本化的请求。
第二段让输出可以拒绝:JSON Schema 与 Pydantic区分解析、字段约束和业务事实,约束生成与解析边界说明生成时约束能帮什么、不能替代什么。JSON 能解析,不意味着团队符合工单事实。JSON Schema 官方教程说明如何描述结构约束;程序还须按当前业务判断。
第三段让失败有停止条件:解析失败、重试与回退区分暂时故障与永久错误,设置次数和总时限;SSE、WebSocket 与流式响应解释屏幕显示了几个字为何不代表结果已经完整。不能把中途断开的片段当成正式参数。
第四段让选择有证据:模型选择、多模型路由与隐私先看数据边界和任务要求;提示版本与 A/B 实验、提示优化与黄金样例要求在同一数据集上比较;本地推理与模型服务再讨论本地运行的硬件、版本和数据责任。开源项目是源码阅读材料,Star 数不能代替适用性证据。
学完本层后,运行一份只读调用器
这份综合程序把消息、字段检查和失败处理接在一起。第一次读总览时先沿目录学习,完成各篇后再回来运行:把 pipeline.py、test_pipeline.py 按下方代码逐个保存到同一个新文件夹,在资源管理器地址栏输入 powershell 打开终端。
完整代码:pipeline.py
"""读取客户求助,检查模型推荐的处理部门与理由。"""
import json
from dataclasses import dataclass
from typing import Callable, Dict, List, Optional
# 部门标识:Billing 为账单组,Technical 为技术支持组,General 为综合客服组。
TEAMS = {"Billing", "Technical", "General"}
CONTRACT = (
"阅读客户求助记录(工单),推荐处理部门并说明理由,把建议交给客服人员确认。"
"根据客户提供的内容判断。Billing 为账单组,Technical 为技术支持组,General 为综合客服组。"
"输出 JSON 对象,字段恰好是 team、reason、missing_information。"
"team 只能为 Billing、Technical、General 或 null;证据不足时为 null,并列出缺少的信息。"
)
@dataclass
class Result:
status: str
suggestion: Optional[Dict[str, object]]
attempts: int
trace: List[str]
def _unique_pairs(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate key: " + key)
result[key] = value
return result
def validate_response(raw: str) -> Dict[str, object]:
"""解析、检查字段和枚举;不声称判断业务事实是否正确。"""
if not isinstance(raw, str):
raise ValueError("response must be text")
try:
value = json.loads(raw, object_pairs_hook=_unique_pairs)
except json.JSONDecodeError as error:
raise ValueError("invalid_json") from error
if not isinstance(value, dict) or set(value) != {"team", "reason", "missing_information"}:
raise ValueError("fields must be exactly team, reason, missing_information")
if value["team"] is not None and (not isinstance(value["team"], str) or value["team"] not in TEAMS):
raise ValueError("team is not allowed")
if not isinstance(value["reason"], str) or not value["reason"].strip():
raise ValueError("reason must be non-empty text")
missing = value["missing_information"]
if not isinstance(missing, list) or any(not isinstance(item, str) or not item.strip() for item in missing):
raise ValueError("missing_information must be a list of non-empty strings")
if value["team"] is None and not missing:
raise ValueError("missing_information is required when team is null")
return value
def classify_ticket(
ticket: Dict[str, str],
transport: Callable[[List[Dict[str, str]]], str],
max_attempts: int = 2,
) -> Result:
"""transport 是注入的只读响应函数;示例不会写入外部系统。"""
title = ticket.get("title", "").strip()
body = ticket.get("body", "").strip()
if not title or not body:
raise ValueError("title and body are required")
if type(max_attempts) is not int or not 1 <= max_attempts <= 3:
raise ValueError("max_attempts must be between 1 and 3")
messages = [
{"role": "developer", "content": CONTRACT},
{"role": "user", "content": json.dumps({"title": title, "body": body}, ensure_ascii=False)},
]
trace = []
for attempt in range(1, max_attempts + 1):
try:
raw = transport(list(messages))
except TimeoutError:
trace.append("timeout")
continue
try:
suggestion = validate_response(raw)
except ValueError as error:
category = "invalid_json" if str(error) == "invalid_json" else "invalid_schema"
trace.append(category)
messages.append({"role": "developer", "content": "上次输出未通过本地校验;仅重新给出符合契约的 JSON。"})
continue
return Result("needs_review" if suggestion["team"] is None else "suggested", suggestion, attempt, trace)
status = "unknown" if trace and all(item == "timeout" for item in trace) else "needs_review"
return Result(status, None, max_attempts, trace)
if __name__ == "__main__":
class DemoTransport:
def __call__(self, messages):
return '{"team":null,"reason":"演示输入没有充分证据","missing_information":["分流规则"]}'
example = classify_ticket({"title": "求助", "body": "帮我看看订单"}, DemoTransport())
print(json.dumps(example.__dict__, ensure_ascii=False))完整代码:test_pipeline.py
"""L1 综合实践:先规定可观察行为,再实现结构化调用器。"""
import json
import unittest
from pipeline import classify_ticket, validate_response
class FakeTransport:
def __init__(self, answers):
self.answers = list(answers)
self.calls = []
def __call__(self, messages):
self.calls.append(messages)
answer = self.answers.pop(0)
if isinstance(answer, Exception):
raise answer
return answer
class PipelineTests(unittest.TestCase):
def test_valid_answer_returns_suggestion_without_write(self):
transport = FakeTransport(['{"team":"Billing","reason":"订单扣款后无记录","missing_information":[]}'])
result = classify_ticket({"title": "账单", "body": "订单扣款后无记录"}, transport)
self.assertEqual(result.status, "suggested")
self.assertEqual(result.suggestion["team"], "Billing")
self.assertEqual(result.attempts, 1)
self.assertEqual(len(transport.calls), 1)
def test_bad_json_can_be_retried_once_with_feedback(self):
transport = FakeTransport(['不是 JSON', '{"team":null,"reason":"信息不足","missing_information":["订单号"]}'])
result = classify_ticket({"title": "求助", "body": "请帮我看看"}, transport)
self.assertEqual(result.status, "needs_review")
self.assertEqual(result.attempts, 2)
self.assertIn("invalid_json", result.trace)
self.assertEqual(transport.calls[1][-1]["role"], "developer")
def test_invalid_team_is_not_accepted(self):
raw = '{"team":"Admin","reason":"我觉得应该处理","missing_information":[]}'
with self.assertRaisesRegex(ValueError, "team"):
validate_response(raw)
def test_extra_fields_are_rejected(self):
raw = '{"team":"Billing","reason":"扣款","missing_information":[],"write":true}'
with self.assertRaisesRegex(ValueError, "fields"):
validate_response(raw)
def test_timeout_exhaustion_returns_unknown(self):
transport = FakeTransport([TimeoutError("first"), TimeoutError("second")])
result = classify_ticket({"title": "登录", "body": "无法登录"}, transport)
self.assertEqual(result.status, "unknown")
self.assertEqual(result.attempts, 2)
self.assertEqual(result.trace, ["timeout", "timeout"])
def test_blank_ticket_rejected_before_transport(self):
transport = FakeTransport([])
with self.assertRaisesRegex(ValueError, "title"):
classify_ticket({"title": "", "body": "无法登录"}, transport)
self.assertEqual(transport.calls, [])
def test_duplicate_keys_are_rejected(self):
raw = '{"team":"Billing","team":"Technical","reason":"冲突","missing_information":[]}'
with self.assertRaisesRegex(ValueError, "duplicate"):
validate_response(raw)
def test_team_array_is_a_validation_error_not_a_crash(self):
raw = '{"team":[],"reason":"没有依据","missing_information":[]}'
with self.assertRaisesRegex(ValueError, "team"):
validate_response(raw)
def test_transport_wrong_type_is_recorded_as_invalid_response(self):
transport = FakeTransport([None])
result = classify_ticket({"title": "求助", "body": "无法登录"}, transport, max_attempts=1)
self.assertEqual(result.status, "needs_review")
self.assertEqual(result.trace, ["invalid_schema"])
def test_boolean_attempt_limit_is_rejected_before_transport(self):
transport = FakeTransport([])
with self.assertRaisesRegex(ValueError, "max_attempts"):
classify_ticket({"title": "求助", "body": "无法登录"}, transport, max_attempts=True)
self.assertEqual(transport.calls, [])
if __name__ == "__main__":
unittest.main()Get-ChildItem
python --version
python -X utf8 pipeline.py
python -X utf8 -m unittest -v test_pipeline.py主程序使用固定本地响应,预期进入 needs_review;十项管线测试覆盖合法建议、坏 JSON 后有限重试、非法团队、多余字段、重复键、超时、空输入和布尔值冒充重试次数等边界。若 py 不存在,先按 Python 环境篇检查解释器;若报找不到 pipeline,确认两个 .py 文件确实与终端在同一个目录。
transport 是可替换的响应函数,目前返回固定文本。它像给调用器准备好的答卷:先让解析、校验与停止条件走通,再把这个函数换成模型接口。
| 观察到的情况 | 本地状态 | 下一步 |
|---|---|---|
| 字段和枚举合法,有团队建议 | suggested | 业务规则或人工复核,再决定是否转派 |
| 信息不足或两次输出仍无效 | needs_review | 人工补证据或修提示 |
| 请求连续超时、结果不能确认 | unknown | 检查请求记录、用量与已发动作 |
在 pipeline.py 底部 DemoTransport.__call__ 的固定 JSON 中,把 team 从 null 改为 "Billing",把 missing_information 改为 [],其余字段保留。保存后运行 python pipeline.py,状态应变成 suggested。演示请求是“帮我看看订单”,这次只观察字段对状态的影响;判断 Billing 是否合理,还需补齐实际故障信息。改回原值并复跑十项测试。
可选:替换成一次真实模型请求
如果你有教学账号,且愿意承担一次真实请求费用,可额外复制下方完整代码,保存为 live_model.py 和 test_live_model.py。先运行下方测试命令,检查五项假响应与默认关闭测试,再阅读 DeepSeek 当前模型与价格 和 Chat Completions 接口,决定是否打开显式开关。适配器会把本地程序契约的 developer 角色转成该接口支持的 system,并保留 user 消息;切换供应商时要重新核对角色语义。默认不发送请求;真实调用仅用虚构工单、最多一发、限制输出长度。价格会变,脚本没有人民币硬上限,不能用它代替账户余额和账单管理。本站也没有用你的密钥进行付费模型实测,所以真实模型质量仍需你自己在脱敏样例上记录和比较。
完整代码:live_model.py
"""可选的一次付费模型调用;默认不执行,测试使用假 HTTP 响应。"""
import json
import os
from urllib.request import Request, urlopen
from pipeline import classify_ticket
class ModelTransport:
def __init__(self, api_key, base_url, model, *, opener=urlopen):
if not isinstance(api_key, str) or not api_key.strip():
raise ValueError("missing API key")
if not isinstance(base_url, str) or not base_url.startswith("https://"):
raise ValueError("base URL must use https")
if not isinstance(model, str) or not model.strip():
raise ValueError("missing model name")
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self.model = model
self.opener = opener
self.usage = None
def __call__(self, messages):
payload = {
"model": self.model,
# 本地管线用 developer 表示程序契约;DeepSeek Chat Completions 只列 system/user/assistant/tool。
"messages": [{**message, "role": "system" if message.get("role") == "developer" else message.get("role")} for message in messages],
"temperature": 0,
"max_tokens": 128,
"response_format": {"type": "json_object"},
"stream": False,
}
request = Request(
self.base_url + "/chat/completions",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": "Bearer " + self.api_key, "Content-Type": "application/json"},
method="POST",
)
with self.opener(request, timeout=20) as response:
result = json.loads(response.read())
choices = result.get("choices")
if not isinstance(choices, list) or not choices:
raise ValueError("missing choices")
first = choices[0]
if first.get("finish_reason") != "stop":
raise ValueError("incomplete response")
content = first.get("message", {}).get("content")
if not isinstance(content, str):
raise ValueError("missing content")
self.usage = result.get("usage")
return content
def main():
if os.getenv("AGENTDEPTH_PAID_CALL") != "YES":
raise SystemExit("未发送请求。确认官方当前价格、账户余额和教学数据后,设置 AGENTDEPTH_PAID_CALL=YES。")
key = os.getenv("AGENTDEPTH_API_KEY") or os.getenv("DEEPSEEK_API_KEY")
transport = ModelTransport(
key,
os.getenv("AGENTDEPTH_BASE_URL", "https://api.deepseek.com"),
os.getenv("AGENTDEPTH_MODEL", "deepseek-flash"),
)
# 只允许一次请求:不让自动重试悄悄增加费用。输入是虚构工单。
result = classify_ticket(
{"title": "虚构订单求助", "body": "付款后页面没有显示订单;没有订单号和付款凭证。"},
transport,
max_attempts=1,
)
print(json.dumps({"result": result.__dict__, "usage": transport.usage}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()完整代码:test_live_model.py
"""不连接网络,验证真实适配器发出的请求与拒绝路径。"""
import json
import unittest
from unittest.mock import patch
from live_model import ModelTransport, main
from pipeline import classify_ticket
class FakeResponse:
def __init__(self, body):
self.body = body
def __enter__(self):
return self
def __exit__(self, *_):
return False
def read(self):
return json.dumps(self.body).encode("utf-8")
class LiveModelTests(unittest.TestCase):
def setUp(self):
self.requests = []
def opener(self, request, timeout):
self.requests.append((request, timeout))
return FakeResponse({
"choices": [{"finish_reason": "stop", "message": {"content": '{"team":null,"reason":"资料不足","missing_information":["订单号"]}'}}],
"usage": {"prompt_tokens": 30, "completion_tokens": 20},
})
def test_sends_bounded_json_request_to_configured_model(self):
transport = ModelTransport("fake-key", "https://api.deepseek.com", "deepseek-flash", opener=self.opener)
raw = transport([{"role": "user", "content": "虚构工单"}])
self.assertIn("missing_information", raw)
request, timeout = self.requests[0]
self.assertEqual(request.full_url, "https://api.deepseek.com/chat/completions")
self.assertEqual(timeout, 20)
self.assertEqual(request.get_header("Authorization"), "Bearer fake-key")
body = json.loads(request.data)
self.assertEqual(body["model"], "deepseek-flash")
self.assertEqual(body["max_tokens"], 128)
self.assertEqual(body["response_format"], {"type": "json_object"})
def test_rejects_truncated_output(self):
def truncated(_request, timeout):
return FakeResponse({"choices": [{"finish_reason": "length", "message": {"content": "{}"}}]})
transport = ModelTransport("fake-key", "https://api.deepseek.com", "deepseek-flash", opener=truncated)
with self.assertRaisesRegex(ValueError, "incomplete"):
transport([{"role": "user", "content": "虚构工单"}])
def test_rejects_non_https_endpoint(self):
with self.assertRaisesRegex(ValueError, "https"):
ModelTransport("fake-key", "http://example.com", "deepseek-flash", opener=self.opener)
def test_pipeline_contract_uses_chat_completion_supported_roles(self):
transport = ModelTransport("fake-key", "https://api.deepseek.com", "deepseek-flash", opener=self.opener)
classify_ticket({"title": "虚构订单", "body": "订单未显示"}, transport, max_attempts=1)
request, _timeout = self.requests[0]
roles = [message["role"] for message in json.loads(request.data)["messages"]]
self.assertEqual(roles, ["system", "user"])
def test_default_mode_does_not_construct_transport_or_send_request(self):
with patch.dict("os.environ", {}, clear=True), patch("live_model.ModelTransport") as transport:
with self.assertRaisesRegex(SystemExit, "未发送请求"):
main()
transport.assert_not_called()
if __name__ == "__main__":
unittest.main()保存四个文件后,在同一个学习目录运行适配器测试:
dir
python -X utf8 -m unittest -v test_live_model.py预期显示 Ran 5 tests 和 OK。这五项检查使用预设响应,分别验证请求字段、截断响应、HTTPS 地址、消息角色转换,以及默认开关关闭时没有发送请求。
决定进行一次付费调用后,在当前 PowerShell 窗口设置密钥和开关,再运行程序:
# 将占位文字替换为自己的教学账号密钥,仅在当前窗口设置。
$env:AGENTDEPTH_API_KEY = '你的教学账号密钥'
$env:AGENTDEPTH_PAID_CALL = 'YES'
try {
python -X utf8 .\live_model.py
} finally {
# 运行结束后清除当前窗口的密钥和调用开关。
Remove-Item Env:AGENTDEPTH_API_KEY -ErrorAction SilentlyContinue
Remove-Item Env:AGENTDEPTH_PAID_CALL -ErrorAction SilentlyContinue
}程序打印 result 和 usage:前者包含处理状态及候选建议,后者是供应商返回的用量。可以对照客户留言检查建议依据,再按官方价格查看这次请求的费用。模型回答会变化,以自己这次收到的结果为准。
回顾模型调用流程
拿一条“订单扣款后无记录”的工单,自己写出任务契约;解释正文里的“忽略前面规则”为何不具有更高指令权;拿一条失败响应指出是解析错、字段错还是业务依据不足;展示超时和信息不足怎样停止。最后用同一组脱敏样例比较两版提示。如果某一步没有证据,就记为未验证。
先运行固定样例,再对照输入、输出和检查步骤,找出各函数怎样影响结果。
参考资料
- JSON Schema:Understanding JSON Schema:结构契约教程。
- Python json 文档:综合练习的解析接口。
- Anthropic:Building effective agents:简单方案与 Agent 工程取舍。
先从消息角色与指令优先级读起;若只查某个概念,可从上面的四段路线直接进入对应章节。完整知识点范围见 L1 层目录。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。