小节五:怎么测试这个 Agent API——不要只在浏览器里看“能不能打开”

一个 POST API,直接浏览器输入 URL 通常不是正确测试方式。

你需要真实发请求。

先用 Flask Test Client 做自动测试

手动 curl 能确认网络链路,自动测试则能把合同固定下来。应用工厂让我们无需启动端口就能测试:

from agent_harness.service.chat_api import (
    create_chat_app,
)


client = create_chat_app().test_client()

success = client.post(
    "/api/v1/chat",
    json={"message": "检查项目"},
)
assert success.status_code == 200
assert success.get_json()["status"] == "completed"

invalid = client.post(
    "/api/v1/chat",
    json={"message": 123},
)
assert invalid.status_code == 400
assert (
    invalid.get_json()["error"]["code"]
    == "INVALID_REQUEST"
)

项目测试还覆盖非法 JSON、数组 Body、内部异常不泄露,以及 X-Request-ID。这些失败路径比“首页能打开”更能说明 API Contract 是否真的稳定。

用 curl

最简单:

curl -X POST \
  http://127.0.0.1:5000/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"检查这个 Python 项目"}'

测试缺失 message

curl -X POST \
  http://127.0.0.1:5000/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{}'

期望:

HTTP 400

响应:

{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "message is required"
  }
}

再补两个容易漏掉的请求:

curl -i -X POST \
  http://127.0.0.1:5001/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{broken'

curl -i -X POST \
  http://127.0.0.1:5001/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{"message":123}'

二者都应返回 HTTP 400,但错误码不同:第一条是 INVALID_JSON,第二条是 INVALID_REQUEST。如果第二条成功了,通常说明 Route 把数字强制转成了字符串。

用 Python requests 调用

如果本机已经安装:

python3 -m pip install requests

客户端代码:

import requests


url = (
    "http://127.0.0.1:5000"
    "/api/v1/chat"
)

payload = {
    "message": (
        "检查这个 Python 项目"
    )
}

response = requests.post(
    url,
    json=payload,
    timeout=30,
)

print(
    response.status_code
)

print(
    response.json()
)

这一步非常重要。

因为它证明,Agent 已经不需要和调用方处于同一个 Python 进程。

只需要网络协议。

以后前端做的也是同一件事

浏览器:

fetch(
  "/api/v1/chat",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      message: "检查这个 Python 项目"
    })
  }
)

本质和 curl、requests 一样。

这就是 API Contract 的价值。

从调用方收到的错误,反推 API 合同

这一节从调用方视角反过来看,其实特别有价值。因为很多接口设计问题,开发者自己在本地测时感觉不到,但一旦遇到真实请求、真实重试、真实错误,就会立刻暴露。下面这张图就是一个很典型的坑:客户端因为超时没收到响应,于是重试;服务端如果没有幂等设计,就可能把同一业务执行两次。

图片[1]-小节五:怎么测试这个 Agent API——不要只在浏览器里看“能不能打开”-大赛人网

如图就把这个问题拆得非常清楚了:第一次请求已经在服务端创建了任务,但客户端不知道;等它发起第二次请求,如果服务端只是把它当作一条新请求处理,就又会多出一个任务。也正因为这样,request_id、幂等键、run_id 这些字段才不是“锦上添花”,而是在真实场景里避免混乱的关键。

我们先给接口发送三个请求:不合法的 JSON、缺少 task 的对象、一个属于其他用户的 run_id。它们分别对应输入解析、业务参数和资源授权问题。接口应返回可理解、稳定的状态与错误码,而不是让客户端从异常堆栈里猜原因。

重复提交也需要约定。用户点击两次按钮,可能希望查同一个任务,也可能真想再次执行。幂等键应绑定身份、接口与规范化参数;同键同参数返回原 Run,同键异参明确报冲突。只有在创建 Run 的持久化操作中保证唯一性,才能应对两个并发请求同时到达。

request_id 和幂等键不要混用。前者标识“一次 HTTP 请求”,重试时通常会变化,用来串联日志;后者标识“同一次业务提交”,重试时必须保持不变,用来避免重复执行。实际 Agent 任务再由 run_id 标识。三个 ID 看起来相似,职责却完全不同。

配套 Flask 服务已经接入这条约定。客户端可以在 Coding 或 Data Run 创建请求中携带 Idempotency-Key:第一次请求返回 201,同键同参数重放返回原 run_id 和 200,响应里的 replayed=true 会明确告诉调用方没有新建任务;同键异参返回 409,不会猜测客户端究竟想覆盖还是重跑。

这条映射保存在 SQLite 的 idempotency_keys 表中,并与 Session、Run、首个 Event、用户 Message 在同一事务创建。事务会先取得 SQLite 写入保留,再检查 Key,避免两个并发请求都在“先查后写”的缝隙里认为 Key 不存在。教学服务没有用户身份体系,所以当前绑定的是 Key、Agent Kind 与规范化请求;接入认证以后,还应把租户或用户身份放进唯一性边界。

curl -X POST http://127.0.0.1:5000/api/v1/data-analysis/runs \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: analysis-20260913-001' \
  -d '{"question":"为什么收入下降?"}'

取消和审批属于控制请求,也需要鉴权、资源归属和当前状态检查。202 表示请求已受理,并不保证动作已经停止。课程中的最小 Flask 示例可以先展示单用户流程,但部署到共享环境前,必须把上述边界接入服务层。

动手检验:模拟重复点击

对同一任务并发提交两次相同幂等键,再查询创建了多少个 Run。参考结果是一个;用同一个键提交不同任务应返回冲突。单纯在 Python 字典中先查再写,未必能通过这个并发实验。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容