FastGPT 是开源的知识库问答和 AI 工作流平台。做好的应用可以通过 OpenAPI 调用,接口格式和 OpenAI 的 chat/completions 基本一致。 这篇做
fastgpt_chat.py:从命令行向应用提问,打印回答和它引用的知识库片段;没有引用时提醒你核对。重点处理 FastGPT 的错误格式:错误码在 body 里,HTTP 状态码不一定可靠。
先看清成品
- 真实调用云服务:无效 Key 返回的是 HTTP 500 加
code: 514 unAuthApiKey,不是 401。脚本据此给出“API Key 无效”的提示。detail=true时从 responseData 里宽松地提取 quoteList,字段变化也不会崩。- 6 项 pytest 通过(含实测的 514 返回、HTTP 200 但 code≠200、非 JSON 的网关错误)。
合规提醒:只用 FastGPT 官方 OpenAPI 和你自己账号的 API Key。官方文档说明 API Key“相当于当前账号在当前团队下的访问凭证”,所以只能放在服务端,不要写进网页或 App。开源版是 Apache 2.0 加附加条件:可以商用,但做类似 FastGPT 的多租户 SaaS、或去掉控制台的 LOGO/版权信息,需要另外取得商业授权(以仓库 LICENSE 为准)。上传到知识库的文档要有权使用,不放个人隐私。
运行说明:box 不能用 Docker,也没有 FastGPT 账号,所以没有部署或调用真实的 FastGPT 应用。真实跑过的是:用无效 Key 调用 FastGPT 云服务(cloud.fastgpt.cn 和 cloud.fastgpt.io,2026-10-21 21:26–21:28,UTC+8),返回原样贴在文中;以及全部本地代码。正常的回答和引用来自我写的 mock,结构照官方文档
detail=true, stream=false的响应示例;官方也说明不同版本 responseData 的字段可能不同。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
fastgpt_chat.py |
命令行提问,输出回答和引用 |
test_fastgpt_chat.py |
6 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| FastGPT | 云服务(cloud.fastgpt.cn / cloud.fastgpt.io)或按官方文档自托管;默认 API 地址 http://localhost:3000/api |
| API Key | 账号 → API 密钥,或 应用 → 发布渠道 → API 访问;形如 fastgpt-xxx |
| Python | 3.9 以上,pip install requests pytest |
| 环境变量 | FASTGPT_BASE_URL(云服务填 https://cloud.fastgpt.cn/api)、FASTGPT_API_KEY、FASTGPT_APP_ID |
关键事实
| 事实 | 来源 |
|---|---|
所有接口用 Authorization: Bearer <apikey> 鉴权;默认 baseUrl http://localhost:3000/api |
官方 OpenAPI 说明 |
推荐在请求体传 appId;也兼容 Bearer <apiKey>-<appId> 的写法 |
同上 |
不传 chatId 时不用 FastGPT 的上下文,完全按 messages;传了则从数据库取会话,只用 messages 最后一条作为问题 |
官方 对话接口 |
detail=true 时非流式结果的 responseData 包含各节点运行结果,字段可能随版本变化 |
同上 |
部分 SDK 需要在 BaseURL 后加 /v1,出现 404 可以加上再试 |
官方 OpenAPI 说明 |
文件结构
83-fastgpt-A-src/
├── fastgpt_chat.py
└── test_fastgpt_chat.py
第 1 步:脚本
"""调用 FastGPT 应用的对话接口(OpenAPI /api/v1/chat/completions),打印回答和引用的知识库片段。
FastGPT 的错误不一定用 HTTP 状态码表达:我实测无效 Key 返回 HTTP 500 + {"code":514,"statusText":"unAuthApiKey"}。
所以这里同时检查 HTTP 状态码和 body 里的 code。
"""
import argparse, json, os, sys, uuid
import requests
UA = "binbho-tutorial/1.0"
KNOWN = {514: "API Key 无效(unAuthApiKey)"}
class FastGPTError(Exception):
pass
def call(base, key, app_id, question, chat_id=None, session=None):
s = session or requests
body = {"appId": app_id, "chatId": chat_id, "stream": False, "detail": True,
"messages": [{"role": "user", "content": question}]}
if not chat_id:
body.pop("chatId") # 不传 chatId:不使用 FastGPT 的会话上下文
r = s.post(base.rstrip("/") + "/v1/chat/completions", json=body, timeout=120,
headers={"Authorization": f"Bearer {key.strip()}", "User-Agent": UA})
try:
d = r.json()
except ValueError:
raise FastGPTError(f"HTTP {r.status_code},返回的不是 JSON:{r.text[:120]}")
code = d.get("code")
if r.status_code >= 400 or (isinstance(code, int) and code != 200):
hint = KNOWN.get(code, d.get("message"))
raise FastGPTError(f"HTTP {r.status_code} code={code} {d.get('statusText', '')}:{hint}")
return d
def answer_of(d):
return d["choices"][0]["message"]["content"]
def quotes_of(d):
"""responseData 是各节点的运行结果,字段随版本变化(官方文档原话),所以宽松地找 quoteList。"""
out = []
for node in d.get("responseData") or []:
for q in node.get("quoteList") or []:
text = (q.get("q") or "") + ("\n" + q["a"] if q.get("a") else "")
out.append({"id": q.get("id"), "source": q.get("sourceName") or q.get("source"), "text": text.strip()})
return out
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("question")
ap.add_argument("--chat-id", help="同一个 chat-id 连续提问会带上下文")
ap.add_argument("--json", action="store_true")
a = ap.parse_args(argv)
base = os.environ.get("FASTGPT_BASE_URL", "http://localhost:3000/api")
key, app = os.environ.get("FASTGPT_API_KEY", ""), os.environ.get("FASTGPT_APP_ID", "")
if not key.strip() or not app:
print("需要 FASTGPT_API_KEY 和 FASTGPT_APP_ID", file=sys.stderr); return 1
try:
d = call(base, key, app, a.question, a.chat_id)
except (FastGPTError, requests.RequestException) as e:
print(f"调用失败:{e}", file=sys.stderr); return 1
qs = quotes_of(d)
if a.json:
print(json.dumps({"answer": answer_of(d), "quotes": qs}, ensure_ascii=False, indent=2)); return 0
print(answer_of(d))
print(f"\n—— 引用 {len(qs)} 条 ——")
for i, q in enumerate(qs, 1):
print(f"[{i}] {q['source'] or '未知来源'}:{q['text'][:80]}")
if not qs:
print("(没有引用知识库:回答可能来自模型自身,需要核对)")
return 0
if __name__ == "__main__":
sys.exit(main())
第 2 步:测试(mock)
"""mock 是我写的,结构按 FastGPT 官方文档“detail=true, stream=false”的响应示例。"""
import pytest
import fastgpt_chat as f
class R:
def __init__(self, code, body):
self.status_code, self._b, self.text = code, body, str(body)
def json(self):
if isinstance(self._b, str):
raise ValueError
return self._b
class S:
def __init__(self, resp):
self.resp, self.sent = resp, None
def post(self, url, json=None, timeout=None, headers=None):
self.sent = (url, json, headers); return self.resp
OK = {"id": "x", "model": "", "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
"choices": [{"message": {"role": "assistant", "content": "年假为 10 天。"}, "finish_reason": "stop", "index": 0}],
"responseData": [
{"moduleName": "知识库搜索", "quoteList": [{"id": "q1", "q": "员工年假多少天?", "a": "入职满一年后每年 10 天。", "sourceName": "员工手册.md"}]},
{"moduleName": "AI 对话", "tokens": 303}]}
def test_ok_and_quotes():
s = S(R(200, OK))
d = f.call("http://localhost:3000/api/", " fastgpt-k\n", "app1", "年假几天", session=s)
assert f.answer_of(d) == "年假为 10 天。"
assert f.quotes_of(d) == [{"id": "q1", "source": "员工手册.md", "text": "员工年假多少天?\n入职满一年后每年 10 天。"}]
url, body, headers = s.sent
assert url == "http://localhost:3000/api/v1/chat/completions"
assert "chatId" not in body and body["detail"] is True and headers["Authorization"] == "Bearer fastgpt-k"
def test_chat_id_kept():
s = S(R(200, OK)); f.call("http://h/api", "k", "a", "q", chat_id="c1", session=s)
assert s.sent[1]["chatId"] == "c1"
def test_invalid_key_real_shape(): # 2026-10-21 实测的返回
s = S(R(500, {"code": 514, "statusText": "unAuthApiKey", "message": "common:code_error.error_message.514", "data": None}))
with pytest.raises(f.FastGPTError, match="API Key 无效"):
f.call("http://h/api", "bad", "a", "q", session=s)
def test_error_code_with_http_200():
with pytest.raises(f.FastGPTError, match="code=500"):
f.call("http://h/api", "k", "a", "q", session=S(R(200, {"code": 500, "statusText": "", "message": "应用不存在"})))
def test_non_json():
with pytest.raises(f.FastGPTError, match="不是 JSON"):
f.call("http://h/api", "k", "a", "q", session=S(R(502, "<html>Bad Gateway</html>")))
def test_no_quotes():
assert f.quotes_of({"choices": OK["choices"]}) == []
第 3 步:运行
# 真实请求(2026-10-21 21:26 UTC+8)
$ curl -s -X POST https://cloud.fastgpt.cn/api/v1/chat/completions -H 'Authorization: Bearer fastgpt-invalid-demo' -H 'Content-Type: application/json' -d '{"chatId":"t1","stream":false,"detail":false,"messages":[{"role":"user","content":"你好"}]}'
{"code":514,"statusText":"unAuthApiKey","message":"common:code_error.error_message.514","data":null} ← HTTP 500
# cloud.fastgpt.io 返回完全相同
$ export FASTGPT_BASE_URL=https://cloud.fastgpt.cn/api FASTGPT_API_KEY=fastgpt-invalid-demo FASTGPT_APP_ID=000000000000000000000000
$ python fastgpt_chat.py "员工年假有几天?"
调用失败:HTTP 500 code=514 unAuthApiKey:API Key 无效(unAuthApiKey)
$ echo $?
1
$ python -m pytest -q
6 passed
# 有真实 Key 时的输出格式(来自 mock,不是真实结果):
年假为 10 天。
—— 引用 1 条 ——
[1] 员工手册.md:员工年假多少天?
入职满一年后每年 10 天。
怎么检查结果?
- 每个回答都看“引用”:没有引用,说明回答不是来自知识库,要核对。
- 同一个
--chat-id连续问两次,第二次的“它”能指代上一题,说明上下文生效了。 - 在 FastGPT 的对话日志里能看到 API 调用的记录。
调试示例
按 HTTP 状态码判断错误会被误导。 我原本打算照常用 r.raise_for_status(),所以先用 curl 看了真实的错误返回(见运行输出):Key 无效时 HTTP 状态是 500,body 是 {"code":514,"statusText":"unAuthApiKey","message":"common:code_error.error_message.514"}。
如果只看状态码,会报 500 Server Error,让人以为是 FastGPT 服务挂了,而不是 Key 写错。message 字段也只是一个翻译键,不是给人看的句子。所以脚本改成:
- 先解析 JSON,同时看 HTTP 状态码和 body 里的
code(≠200 就算失败); - 用
KNOWN把已经确认的错误码翻译成中文提示(目前只放了我实测过的 514); - 返回不是 JSON(例如反向代理的 502 页面)时单独提示。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| code 514 unAuthApiKey | Key 无效或已删除 | 重新复制 Key,注意别带空格 |
| 404 | baseUrl 少了 /api 或 /v1 | 核对 FASTGPT_BASE_URL |
| 回答和知识库无关 | 没检索到,模型自由发挥 | 看引用;在应用里调高相似度或开“无引用时回复” |
| 上下文串了 | 多个用户共用同一个 chatId | 每个用户的会话用自己的 chatId |
模板规则
- Key 只放服务端环境变量。
- 同时检查 HTTP 状态和 body code。
- 没有引用的回答要标记出来。
- chatId 由自己的系统生成,保证唯一。
项目资源
| 用途 | 地址 |
|---|---|
| FastGPT OpenAPI 说明(官方) | https://doc.fastgpt.io/zh-CN/openapi/intro |
| FastGPT 对话接口(官方) | https://doc.fastgpt.io/zh-CN/openapi/chat |
| FastGPT GitHub | https://github.com/labring/FastGPT |
| FastGPT 许可证 | https://github.com/labring/FastGPT/blob/main/LICENSE |
先用命令行把问答和引用跑通,再接进你的服务端。