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

L1 总览:把模型回答变成可检查的接口

我的学习 →

正在读取学习记录…

L1 学的是程序怎样与模型交换信息:先组成请求,再接收回答,按约定检查,最后决定是否使用。模型回答是一份候选建议;真正执行写入的是业务程序。L0 已让 Python、HTTP 和任务边界有了基础,这一层把它们接到模型接口上。

先看一个客户求助:“付款成功,但订单页还是空的。”客服系统把这条求助保存成一条记录,称为“工单”。模型读完后推荐账单组(代码中记作 Billing),并说明理由:“付款与订单记录不一致,需要核对账单。”程序检查推荐部门和理由,再把建议展示给客服人员,由客服人员决定交给谁处理。后面逐步讨论信息不足时如何追问、请求超时后如何处理。

L1 沿用这个客服例子,学习怎样把任务和客户留言发给模型、怎样检查它返回的部门与理由,以及怎样记录调用失败。这些步骤也能用在邮件分类、资料整理等任务中。

十二项知识点怎样接起来

L1 十二个知识点分为提出任务、校验输出、处理失败和比较选择四段
图1:十二个知识点分成四段:提出任务、校验输出、处理失败、比较选择。

一直带着上面那条“付款成功却看不到订单”的工单读。每读完一段,就问自己一个新问题:

读到哪里此时要解决的问题手里应该多出什么
提出任务:消息、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
Python
"""读取客户求助,检查模型推荐的处理部门与理由。"""

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
Python
"""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()
PowerShell
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 三种状态,均不自动写工单
图2:suggested 仍待业务核对;unknown 不能被界面误写为成功。
观察到的情况本地状态下一步
字段和枚举合法,有团队建议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
Python
"""可选的一次付费模型调用;默认不执行,测试使用假 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
Python
"""不连接网络,验证真实适配器发出的请求与拒绝路径。"""

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

保存四个文件后,在同一个学习目录运行适配器测试:

PowerShell
dir
python -X utf8 -m unittest -v test_live_model.py

预期显示 Ran 5 tests 和 OK。这五项检查使用预设响应,分别验证请求字段、截断响应、HTTPS 地址、消息角色转换,以及默认开关关闭时没有发送请求。

决定进行一次付费调用后,在当前 PowerShell 窗口设置密钥和开关,再运行程序:

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:前者包含处理状态及候选建议,后者是供应商返回的用量。可以对照客户留言检查建议依据,再按官方价格查看这次请求的费用。模型回答会变化,以自己这次收到的结果为准。

回顾模型调用流程

拿一条“订单扣款后无记录”的工单,自己写出任务契约;解释正文里的“忽略前面规则”为何不具有更高指令权;拿一条失败响应指出是解析错、字段错还是业务依据不足;展示超时和信息不足怎样停止。最后用同一组脱敏样例比较两版提示。如果某一步没有证据,就记为未验证。

先运行固定样例,再对照输入、输出和检查步骤,找出各函数怎样影响结果。

参考资料

先从消息角色与指令优先级读起;若只查某个概念,可从上面的四段路线直接进入对应章节。完整知识点范围见 L1 层目录。

图片查看器
100%

文章配图 · 按 Esc 关闭