Agent 应用开发会员账号
知识目录选择核心方向与细分内容

SYSTEMATIC LEARNING / 分层专题

第一次模型调用与响应契约

从普通 HTTP 接口走到生成接口:组装请求、分辨响应、关联工具结果,再用运行证据核对答案。

学习目标:能组装一个模型请求,分别处理文本、工具候选、拒绝、不完整与 HTTP 错误,并跑通有预算的只读工具循环。

内容核对 2026-10-04 · 每层有独立讲解、任务与检查

按这个知识点的熟悉程度选择起点。当前层:进阶 · 定位故障。完成任务后可以继续下一层;阅读与自检不自动代表掌握。

本层学习目录

先补齐必要概念

适合:已跑通正常链条,开始处理异常响应与错误答案。

模型请求
包含模型 ID、任务指令、用户输入及可用工具;凭据与真实用户身份由调用端管理。
候选动作
模型给出的工具名与参数。它表达建议,执行端还要核对格式、权限和业务条件。
调用关联
call_id 把某次函数请求与它的返回结果关联;业务幂等键需由业务另外定义。
完成状态
HTTP、生成过程和业务任务各有状态;只有业务验收能说明任务完成。
响应样本
本实验由作者编写的 Responses 形状样本,用于检查程序分支;真实 API 以当次返回和官方契约为准。

原理怎样一步步成立?

  1. 组装完整请求

    任务指令说明允许行为,用户输入描述目标,工具定义提供局部参数契约。

  2. 分类接口结果

    先检查 HTTP 结果,再检查生成状态和 output 的条目类型,保留拒绝与不完整原因。

  3. 执行受限动作

    从可信会话绑定身份,独立校验参数、访问权限和调用预算;本实验只允许读取订单。

  4. 回传与核对

    保留原始输出条目,把函数结果以对应 call_id 回传,最后对照工具回执核对候选答案。

进阶 · 定位故障

沿状态和回执定位失败

本层目标:用明确失败样本定位传输、生成、参数、授权及答案核对问题。

先标出失败发生在哪个边界

把排查记录分成:HTTP 请求、生成状态、候选参数、资源授权、工具回执、最终答案。每段保存一个可核对结果。HTTP 429 说明这次接口请求受到限制;工具已成功但后续生成被截断,则保留成功回执并调整解释阶段。两个现象的恢复动作不同。

复现四个最小故障

在实验包目录运行:

python3 - <<'PY'
from model_response_contract import run_loop, tool_response, text_response
print(run_loop([tool_response(name='refund_order')])['status'])
print(run_loop([tool_response('o-2')])['status'])
print(run_loop([tool_response(), tool_response()])['status'])
print(run_loop([text_response({'order_id':'o-1','status':'delivered','source':'read_order'})])['status'])
PY

依次观察未知工具、权限拒绝、重复调用 ID 和缺少回执的答案。最后一条的文字与教学订单状态碰巧一致,仍得到 unverified_answer,因为程序没有在这条运行中获得可信查询证据。

不完整输出怎样处理

将工具响应的 status 改为 incomplete,即使 output 里已有看似完整的参数,本实验也以不完整结束,不执行候选。对于真实流式系统,依据完整调用条目的事件边界校验;网络中断、模型达到输出上限和业务动作已提交,要分别记录。恢复时先确认哪些事实已发生,再决定重做生成或查询外部回执。

看到 200 但解析失败

检查 HTTP 返回体是否确为目标 API、顶层状态和 output 类型是否匹配适配器。HTML 错误页、代理返回或另一个供应商的相似 JSON 都需要独立分支。不要通过删除字符、补闭合括号或默默换接口来伪装成功。记录固定错误类别和版本,敏感正文按业务规则脱敏。

重试需要明确对象

纯生成请求的临时错误可在费用与时间预算内退避;已有供应商动作或应用写入时,先核对实际效果。本课不实现远端重试协议,恢复课提供未知结果的独立实验。排错目标是识别可重试对象及已确认事实,并给出下一步验收,而不是单纯增加重试次数。

运行实验,观察反例

使用作者编写的非流式 Responses 形状样本、教学订单与可信身份模拟,验证请求组装、只读校验、call_id 回传、预算和答案核对。程序不调用在线模型或真实订单服务。

Python 3.10+ · 默认运行只使用标准库 · 在你的电脑运行

  1. 下载本页的 Agent 应用入门实验包,解压后进入 agent-application-lab-v1 目录。
  2. 使用 Python 3.10+ 执行上方命令;默认回放只需标准库与包内数据。
  3. 对照输出与检查点,再运行 python3 -m unittest test_application -v,并完成当前层任务。
下载完整应用实验包(含数据与依赖脚本) ↓
python3 model_response_contract.py
查看本入口脚本
"""A bounded read-only tool loop using authored Responses-shaped fixtures.

This file makes no API requests. The course separately documents the real API request.
The fixture response shape is deliberately limited to the non-streaming cases below.
"""
import json

ORDERS = {
    "o-1": {"tenant": "shop-a", "user": "u-1", "status": "delivered"},
    "o-2": {"tenant": "shop-b", "user": "u-2", "status": "processing"},
}
PRINCIPAL = {"tenant": "shop-a", "user": "u-1"}
TOOL = {
    "type": "function", "name": "read_order", "description": "Read an authorized order status.",
    "strict": True, "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
                                    "required": ["order_id"], "additionalProperties": False},
}


def build_request(order_id, model="reader-selected-model"):
    return {"model": model, "input": [
        {"role": "developer", "content": "Use read_order for order facts. Tool access is checked by the application. Return an object with order_id, status and source. Do not perform writes."},
        {"role": "user", "content": "What is the status of order " + order_id + "?"},
    ], "tools": [TOOL], "max_output_tokens": 500}


def classify_response(response, http_status=200):
    if http_status != 200:
        return {"kind": "http_error", "retryable": http_status == 429 or 500 <= http_status < 600}
    if not isinstance(response, dict):
        return {"kind": "unsupported"}
    status = response.get("status")
    if status == "incomplete":
        return {"kind": "incomplete", "reason": response.get("incomplete_details")}
    if status in ("failed", "cancelled"):
        return {"kind": status}
    if status != "completed" or not isinstance(response.get("output"), list):
        return {"kind": "unsupported"}
    calls, texts = [], []
    for item in response["output"]:
        if not isinstance(item, dict):
            return {"kind": "unsupported"}
        if item.get("type") == "function_call":
            if not all(isinstance(item.get(key), str) and item[key] for key in ("call_id", "name", "arguments")):
                return {"kind": "invalid_tool_call"}
            calls.append(item)
        elif item.get("type") == "message":
            if item.get("status") not in (None, "completed") or not isinstance(item.get("content"), list):
                return {"kind": "unsupported"}
            for content in item["content"]:
                if not isinstance(content, dict):
                    return {"kind": "unsupported"}
                if content.get("type") == "refusal":
                    return {"kind": "refused"}
                if content.get("type") != "output_text" or not isinstance(content.get("text"), str):
                    return {"kind": "unsupported"}
                texts.append(content["text"])
        elif item.get("type") != "reasoning":
            return {"kind": "unsupported"}
    if calls:
        return {"kind": "tool_requests", "calls": calls}
    return {"kind": "text", "text": "".join(texts)} if texts else {"kind": "empty"}


def execute_read(call, principal):
    if call["name"] != "read_order":
        raise ValueError("unknown_tool")
    try:
        args = json.loads(call["arguments"])
    except (ValueError, TypeError):
        raise ValueError("invalid_arguments") from None
    if not isinstance(args, dict) or set(args) != {"order_id"} or not isinstance(args["order_id"], str):
        raise ValueError("invalid_arguments")
    order = ORDERS.get(args["order_id"])
    if not order or order["tenant"] != principal["tenant"] or order["user"] != principal["user"]:
        # No protected resource fields are returned when access is denied.
        raise ValueError("permission_denied")
    return {"order_id": args["order_id"], "status": order["status"], "source": "read_order"}


def run_loop(responses, principal=None, max_turns=3, max_calls=2):
    principal = principal or PRINCIPAL
    trace, results, seen_ids, calls_used = [], [], set(), 0
    transcript = build_request("o-1")["input"]
    for turn, response in enumerate(responses, 1):
        if turn > max_turns:
            break
        event = classify_response(response)
        trace.append({"turn": turn, "kind": event["kind"]})
        if event["kind"] == "tool_requests":
            # Preserve output items, including reasoning items when present, before
            # appending call-correlated tool results to the next request input.
            transcript.extend(response["output"])
            if calls_used + len(event["calls"]) > max_calls:
                return {"status": "budget_exhausted", "trace": trace, "results": results, "calls": calls_used}
            for call in event["calls"]:
                if call["call_id"] in seen_ids:
                    return {"status": "duplicate_call_id", "trace": trace, "results": results, "calls": calls_used}
                seen_ids.add(call["call_id"])
                calls_used += 1
                try:
                    result = execute_read(call, principal)
                except ValueError as error:
                    return {"status": str(error), "trace": trace, "results": results, "calls": calls_used}
                result_item = {"type": "function_call_output", "call_id": call["call_id"], "output": json.dumps(result, sort_keys=True)}
                results.append(result_item)
                transcript.append(result_item)
            trace[-1]["nextInputTypes"] = [item.get("type", "message") for item in transcript]
        elif event["kind"] == "text":
            try:
                candidate = json.loads(event["text"])
            except ValueError:
                candidate = None
            confirmed = isinstance(candidate, dict) and set(candidate) == {"order_id", "status", "source"} and any(candidate == json.loads(result["output"]) for result in results)
            return {"status": "answer_ready" if confirmed else "unverified_answer", "trace": trace, "results": results, "calls": calls_used}
        else:
            return {"status": event["kind"], "trace": trace, "results": results, "calls": calls_used}
    return {"status": "budget_exhausted", "trace": trace, "results": results, "calls": calls_used}


def tool_response(order_id="o-1", call_id="call-1", name="read_order"):
    return {"status": "completed", "output": [{"type": "function_call", "call_id": call_id, "name": name, "arguments": json.dumps({"order_id": order_id})}]}


def text_response(candidate):
    return {"status": "completed", "output": [{"type": "message", "role": "assistant", "status": "completed", "content": [{"type": "output_text", "text": json.dumps(candidate)}]}]}


def demo():
    normal = run_loop([tool_response(), text_response({"order_id": "o-1", "status": "delivered", "source": "read_order"})])
    denied = run_loop([tool_response("o-2")])
    loop = run_loop([tool_response(call_id="c-" + str(i)) for i in range(5)], max_turns=2)
    refused = {"status": "completed", "output": [{"type": "message", "content": [{"type": "refusal", "refusal": "fixture refusal"}]}]}
    return {"normal": normal["status"], "toolResultCallId": normal["results"][0]["call_id"],
            "crossTenant": denied["status"], "deniedResults": len(denied["results"]),
            "boundedLoop": loop["status"], "boundedCalls": loop["calls"],
            "refusal": classify_response(refused)["kind"],
            "incomplete": classify_response({"status": "incomplete", "incomplete_details": {"reason": "max_output_tokens"}})["kind"],
            "scope": "authored_responses_fixtures_no_api"}


if __name__ == "__main__":
    print(json.dumps(demo(), ensure_ascii=False, sort_keys=True))

本地运行的预期输出

{"boundedCalls": 2, "boundedLoop": "budget_exhausted", "crossTenant": "permission_denied", "deniedResults": 0, "incomplete": "incomplete", "normal": "answer_ready", "refusal": "refused", "scope": "authored_responses_fixtures_no_api", "toolResultCallId": "call-1"}
  • normal=answer_ready,最终对象与该次 read_order 回执一致。
  • crossTenant=permission_denied 且 deniedResults=0。
  • boundedLoop=budget_exhausted 且 boundedCalls=2。
  • refusal 与 incomplete 使用独立处理路径。
查看运行环境、输出和校验记录 →

本层验收任务

交付四个故障的最小输入、状态、回执数量和定位结论;另分析工具成功后模型输出不完整的恢复步骤。

完成后逐条核对

  • 每项故障都能用包内函数复现。
  • 额外字段和越权输入分别归到参数与授权。
  • 缺少工具回执时,文字正确也不直接通过。
  • 后续生成失败保留已确认的工具事实。

保存自己的过程、代码与结果。这里提供验收要求,暂不自动评分或保存课程掌握状态。

收起答案,检查理解

模型请求超时后,能否一律重发整个工具循环?

延伸原理与知识练习

遇到不熟悉的原理,先阅读实现、连续追问和迁移案例,再独立说明前提与边界。作答与笔记保存到原有账号记录。

本专题的全部关联解析与练习(2 道)

依据与验证范围

原理依据来自公开资料;数字、案例和任务是本站教学设计。离线实验验证本页注明的范围,学习效果仍需通过独立任务与反馈判断。