一个 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——不要只在浏览器里看“能不能打开”-大赛人网](https://www.dsrw.com/wp-content/uploads/2026/11/image-1605.png)
如图就把这个问题拆得非常清楚了:第一次请求已经在服务端创建了任务,但客户端不知道;等它发起第二次请求,如果服务端只是把它当作一条新请求处理,就又会多出一个任务。也正因为这样,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 字典中先查再写,未必能通过这个并发实验。
















请登录后查看评论内容