示例与测试代码
快速联调顺序
建议按这个顺序做 smoke test:
- 调
GET /credits - 调
GET /models - 用
POST /assignments创建学生 assignment;如果不想创建 assignment,就走POST /evaluations/one-pass - 用返回的
assignment_id调POST /evaluations - 再轮询
GET /evaluations/:taskId/status - 再拿最终结果
GET /evaluations/:taskId - 再调
POST /feedback-rewrite - 如果你还需要新建 marking model,再测试
questions-ingest-preview、rubrics-ingest-preview和POST /models
如果前两步正常,但 evaluation 失败,最常见的原因是把 model_id 当成了 assignment_id、所选 model 还没 ready,或者客户端还停留在旧的 X-TOKEN-KEY 流程。
这些示例里的 API key 是以 kct_ 开头的 KEATH Public API key。Gemini、NewAPI 这类模型供应商 key 只应该留在 KEATH 服务端,不应该从你的客户端传给 Public API。
推荐测试文本
当源文件里有多个题目、封面页或 sample answer 时,可以先用这些 specification 测试。
题目解析:
只解析 Question 2。忽略封面页、sample answer 和 teacher notes。如果 PDF 里有多个任务,只聚焦 situational writing 这一题。Rubric 解析:
只使用目标写作任务对应的 rubric 表格。忽略 sample answer、解释性说明和其他 worksheet。Feedback Rewrite:
把 feedback 改得更短、更清晰、更像老师直接给学生的反馈,但不要改变原有判断。推荐测试作文文本
如果你想验证 POST /evaluations,又不想直接用真实学生作文,可以先用这一段安全样例:
Dear Mrs Tan,
I am writing to request permission for our class to organise a recycling drive next Friday after school. Many students have noticed that large amounts of paper bottles and food packaging are thrown away every day. We believe a short class project would help students build better habits and understand why recycling matters.
If the school approves the activity our class can prepare labelled collection boxes and take turns to explain the instructions to other students. We can also make a short announcement during assembly so that everyone knows what items can be collected.
I hope you will consider this proposal. Thank you for your time and support.
Yours sincerely,
Jamie Leecurl 示例
查询 credits
curl "https://keath.ai/api/keath/public/v1/credits" \
-H "X-API-Key: kct_your_api_key"直接上传 PDF 解析题目
curl -X POST "https://keath.ai/api/keath/public/v1/questions-ingest-preview" \
-H "X-API-Key: kct_your_api_key" \
-F "file=@./question-pack.pdf;type=application/pdf" \
-F "specification=只解析 Question 2,忽略 sample answer 页面。"如果你的 HTTP 客户端不能设置 type=application/pdf,KEATH 会尝试根据文件扩展名兜底识别常见 PDF / 图片类型。仍然建议显式传 MIME type,这样更清晰,也更不容易被代理或 SDK 改坏。
上传图片并混合已有 URL
curl -X POST "https://keath.ai/api/keath/public/v1/questions-ingest-preview" \
-H "X-API-Key: kct_your_api_key" \
-F "files=@./scan-1.jpg;type=image/jpeg" \
-F 'assets=[{"url":"https://cdn.example.com/supporting-page.pdf","mime_type":"application/pdf","name":"supporting-page.pdf"}]' \
-F "specification=以上传图片为目标题目。"直接上传 rubric
curl -X POST "https://keath.ai/api/keath/public/v1/rubrics-ingest-preview" \
-H "X-API-Key: kct_your_api_key" \
-F "file=@./rubric.pdf;type=application/pdf" \
-F "total_score=30" \
-F "cum_method=sum" \
-F "specification=只用第一页的 rubric 表格。"用 model 创建 assignment
curl -X POST "https://keath.ai/api/keath/public/v1/assignments" \
-H "X-API-Key: kct_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model_id": 930,
"assignment_name": "Situation Writing Test",
"assignment_desc": "Student-facing writing assignment",
"project_subject": "English",
"deadline_time": "2026-06-01T00:00:00.000Z",
"expected_number": 30,
"file_type": "pdf"
}'提交一次评测
curl -X POST "https://keath.ai/api/keath/public/v1/evaluations" \
-H "X-API-Key: kct_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"assignment_id": 1620,
"paper_content": "Dear Mrs Tan,\n\nI am writing to request permission for our class to organise a recycling drive next Friday after school...",
"style": "Bullet"
}'上传学生作答文件提交评测
curl -X POST "https://keath.ai/api/keath/public/v1/evaluations" \
-H "X-API-Key: kct_your_api_key" \
-F "assignment_id=1620" \
-F "answer_file=@./student-answer.pdf;type=application/pdf" \
-F "student_id=anon-student-001" \
-F "style=Bullet"One-pass 一次性评测
curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/one-pass" \
-H "X-API-Key: kct_your_api_key" \
-H "Idempotency-Key: eval-student-001-attempt-1" \
-F "model_id=930" \
-F "question_file=@./question.pdf;type=application/pdf" \
-F "rubric_file=@./rubric.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
-F "answer_file=@./student-answer.txt;type=text/plain" \
-F "student_id=anon-student-001" \
-F "specification=只使用 Question 2。" \
-F "style=Bullet"接口返回 HTTP 202。请保存公开 task_id,不要在这次请求里等待评分结果。
批量 one-pass 评测
curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/batch" \
-H "X-API-Key: kct_your_api_key" \
-H "Idempotency-Key: class-5a-writing-2026-07-15" \
-H "Content-Type: application/json" \
-d '{
"evaluations": [
{"model_id":930,"question_text":"写一封正式信件。","paper_content":"尊敬的校长:……","student_id":"anon-001"},
{"model_id":930,"question_text":"写一封正式信件。","paper_content":"尊敬的校长,我建议……","student_id":"anon-002"}
],
"callback_url":"https://integration.example.com/keath/results"
}'轮询评测状态
curl "https://keath.ai/api/keath/public/v1/evaluations/task_123/status" \
-H "X-API-Key: kct_your_api_key"获取评测结果
curl "https://keath.ai/api/keath/public/v1/evaluations/task_123" \
-H "X-API-Key: kct_your_api_key"取消评测
curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/task_123/cancel" \
-H "X-API-Key: kct_your_api_key"Feedback Rewrite
curl -X POST "https://keath.ai/api/keath/public/v1/feedback-rewrite" \
-H "X-API-Key: kct_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"style": "bullet_points",
"instruction": "把内容写得更短一点,更像老师给学生的反馈。",
"sections": [
{
"item": "Content",
"comment": "Your answer contains relevant ideas but the explanation remains underdeveloped.",
"score": 7,
"max_score": 10
}
]
}'JavaScript 示例
const apiKey = process.env.KEATH_API_KEY
const baseUrl = 'https://keath.ai/api/keath/public/v1'
async function getCredits() {
const response = await fetch(`${baseUrl}/credits`, {
headers: {
'X-API-Key': apiKey,
},
})
return response.json()
}
async function previewQuestionsWithPdf(file) {
const formData = new FormData()
formData.append('file', file, file.name)
formData.append('specification', '只解析 Question 2,忽略 sample answer 页面。')
const response = await fetch(`${baseUrl}/questions-ingest-preview`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
},
body: formData,
})
return response.json()
}
async function createAssignment(modelId) {
const response = await fetch(`${baseUrl}/assignments`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model_id: modelId,
assignment_name: 'Situation Writing Test',
assignment_desc: 'Student-facing writing assignment',
project_subject: 'English',
deadline_time: '2026-06-01T00:00:00.000Z',
expected_number: 30,
file_type: 'pdf',
}),
})
return response.json()
}
async function submitEvaluation(assignmentId, paperContent) {
const response = await fetch(`${baseUrl}/evaluations`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
assignment_id: assignmentId,
paper_content: paperContent,
style: 'Bullet',
}),
})
return response.json()
}
async function submitEvaluationWithFile(assignmentId, answerFile) {
const formData = new FormData()
formData.append('assignment_id', String(assignmentId))
formData.append('answer_file', answerFile, answerFile.name)
formData.append('student_id', 'anon-student-001')
formData.append('style', 'Bullet')
const response = await fetch(`${baseUrl}/evaluations`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
},
body: formData,
})
return response.json()
}
async function submitOnePassEvaluation({ modelId, questionFile, rubricFile, answerFile, requestId }) {
const formData = new FormData()
formData.append('model_id', String(modelId))
formData.append('question_file', questionFile, questionFile.name)
formData.append('rubric_file', rubricFile, rubricFile.name)
formData.append('answer_file', answerFile, answerFile.name)
formData.append('student_id', 'anon-student-001')
formData.append('specification', '只使用 Question 2。')
formData.append('style', 'Bullet')
const response = await fetch(`${baseUrl}/evaluations/one-pass`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Idempotency-Key': requestId,
},
body: formData,
})
return response.json()
}
async function pollEvaluationUntilDone(taskId, options = {}) {
const maxAttempts = options.maxAttempts ?? 20
const initialDelayMs = options.initialDelayMs ?? 3000
const maxDelayMs = options.maxDelayMs ?? 15000
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await fetch(`${baseUrl}/evaluations/${taskId}/status`, {
headers: {
'X-API-Key': apiKey,
},
})
const payload = await response.json()
const status = payload.data?.status || payload.status
if (status === 'SUCCESS' || status === 'FAILURE' || status === 'CANCEL') {
return payload
}
const delayMs = Math.min(initialDelayMs * attempt, maxDelayMs)
await new Promise(resolve => setTimeout(resolve, delayMs))
}
throw new Error(`评测任务 ${taskId} 在 ${maxAttempts} 次轮询后仍未结束`)
}Python 示例
import json
import time
import requests
API_KEY = "kct_your_api_key"
BASE_URL = "https://keath.ai/api/keath/public/v1"
def get_credits():
response = requests.get(
f"{BASE_URL}/credits",
headers={"X-API-Key": API_KEY},
timeout=60,
)
response.raise_for_status()
return response.json()
def preview_questions_with_pdf(path: str):
with open(path, "rb") as f:
response = requests.post(
f"{BASE_URL}/questions-ingest-preview",
headers={"X-API-Key": API_KEY},
files={"file": ("question-pack.pdf", f, "application/pdf")},
data={"specification": "只解析 Question 2,忽略 sample answer 页面。"},
timeout=300,
)
response.raise_for_status()
return response.json()
def preview_questions_mixed(path: str):
with open(path, "rb") as f:
response = requests.post(
f"{BASE_URL}/questions-ingest-preview",
headers={"X-API-Key": API_KEY},
files={"file": ("scan-1.jpg", f, "image/jpeg")},
data={
"assets": json.dumps(
[
{
"url": "https://cdn.example.com/supporting-page.pdf",
"mime_type": "application/pdf",
"name": "supporting-page.pdf",
}
]
),
"specification": "以上传图片为目标题目。",
},
timeout=300,
)
response.raise_for_status()
return response.json()
def submit_evaluation(assignment_id: int, paper_content: str):
response = requests.post(
f"{BASE_URL}/evaluations",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
json={
"assignment_id": assignment_id,
"paper_content": paper_content,
"style": "Bullet",
},
timeout=300,
)
response.raise_for_status()
return response.json()
def submit_evaluation_with_file(assignment_id: int, answer_path: str):
with open(answer_path, "rb") as f:
response = requests.post(
f"{BASE_URL}/evaluations",
headers={"X-API-Key": API_KEY},
data={
"assignment_id": str(assignment_id),
"student_id": "anon-student-001",
"style": "Bullet",
},
files={"answer_file": ("student-answer.pdf", f, "application/pdf")},
timeout=300,
)
response.raise_for_status()
return response.json()
def submit_one_pass_evaluation(
model_id: int,
question_path: str,
rubric_path: str,
answer_path: str,
request_id: str,
):
with open(question_path, "rb") as question, open(rubric_path, "rb") as rubric, open(answer_path, "rb") as answer:
response = requests.post(
f"{BASE_URL}/evaluations/one-pass",
headers={
"X-API-Key": API_KEY,
"Idempotency-Key": request_id,
},
data={
"model_id": str(model_id),
"student_id": "anon-student-001",
"specification": "只使用 Question 2。",
"style": "Bullet",
},
files={
"question_file": ("question.pdf", question, "application/pdf"),
"rubric_file": (
"rubric.docx",
rubric,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
),
"answer_file": ("student-answer.txt", answer, "text/plain"),
},
timeout=300,
)
response.raise_for_status()
return response.json()
def wait_for_evaluation(task_id: str):
max_attempts = 20
initial_delay_seconds = 3
max_delay_seconds = 15
for attempt in range(1, max_attempts + 1):
response = requests.get(
f"{BASE_URL}/evaluations/{task_id}/status",
headers={"X-API-Key": API_KEY},
timeout=60,
)
response.raise_for_status()
payload = response.json()
data = payload.get("data", payload)
status = data["status"]
if status in {"SUCCESS", "FAILURE", "CANCEL"}:
return payload
delay_seconds = min(initial_delay_seconds * attempt, max_delay_seconds)
time.sleep(delay_seconds)
raise TimeoutError(
f"评测任务 {task_id} 在 {max_attempts} 次轮询后仍未结束"
)排查清单
- 确认请求地址是
/api/keath/public/v1/... - 确认 Header 用的是
X-API-Key,不是X-TOKEN-KEY - 确认你使用的是 KEATH Public API key,即
kct_...,不是 NewAPIsk-...这类模型供应商 key - 确认
model_id来自GET /models - 确认
assignment_id来自POST /assignments或产品 assignment 详情页 URL,不是来自GET /models - 确认创建 assignment 或 one-pass evaluation 前,所选 model 已经是
ready - 如果上传学生作答文件,确认字段名使用
answer_file等受支持字段 - 如果是 one-pass 请求,确认
model_id来自GET /models - 如果 multipart 里同时传了
assets,确认它是一个 JSON 数组字符串 - 如果 multipart 评测请求里传了
rubrics或current_feedbacks,确认它们是 JSON 数组字符串 - 上传文件时优先显式传支持的 MIME type;如果客户端传不了,就确保文件扩展名是支持的格式
- 确认每个文件都小于
20 MB - Google Docs 需要先导出成 DOCX、PDF 或纯文本再上传;Public v1 还没有直接 Google Docs OAuth 导入