qi-harness 解决什么问题
奇语标准库里的 标准库.大模型 已经能「调一次模型」:给它一段提示,拿回一段文本。但真实的 Agent 不止于此 —— 你需要知道每次调用花了多久、模型有没有要求调工具、限流时要不要退避重试、改了提示词后行为是否回归。这些「外壳」工作,正是 qi-harness 的职责。
它的设计借鉴了 Martin Fowler 的 harness engineering 思路:把不确定的 LLM 调用包在确定的、可调试的工程外壳里。具体提供四件事:
- Agent 循环:模型返回
tool_calls时,自动派发工具、回写结果、继续对话,直到给出最终文本 —— 不用手写「调 → 看 → 执行工具 → 再调」的状态机。 - 追踪:每次模型调用、每次工具派发都 emit 一条 JSON 事件,默认打到 stdout。
- 重试:429 限流 / 网络抖动 / 空响应自动指数退避。
- 一致的会话/代理抽象:配置一次端点,复用为一次性问答、流式问答、带工具的多步代理。
配置与凭证
qi-harness 是一个用奇语写成的库,作为跨包依赖通过 destructure 导入。配置任何 OpenAI 兼容端点只需一行 大模型(端点, 密钥, 模型),DeepSeek、OpenAI、本地推理服务都一样。
导入 标准库.操作系统 作为 系统;
导入 Harness::{ 模型配置, 大模型, 配置系统提示, 配置温度, 开启会话, 关闭会话Harness };
// 密钥从环境变量读 —— 永远不要硬编码
变量 密钥: 字符串 = 系统.获取环境变量("QI_LLM_KEY");
变量 配置: 模型配置 = 大模型("https://api.deepseek.com", 密钥, "deepseek-chat");
配置 = 配置系统提示(配置, "你是 qi 编程语言的简洁中文助手。");
配置 = 配置温度(配置, "0.3"); // 可选;还有 配置最大令牌(配置, 2048)
变量 会话: 整数 = 开启会话(配置); // 返回 >0 即成功系统.获取环境变量("QI_LLM_KEY"),密钥为空就优雅退出。本文的示例还会回退读取 DEEPSEEK_API_KEY,方便复用已有环境。运行需要一个有效的密钥和网络;没有它你看不到真实的模型回复(见文末「运行说明」)。第一个对话
会话开好之后,用它创建一个「代理」。最简单的用法是 简单问:一次性问答,不带工具,返回模型的完整文本。
导入 Harness::{ 创建代理, 设置最大步数, 简单问, 关闭代理 };
变量 代理值: 代理 = 创建代理("教程助手", 会话);
代理值 = 设置最大步数(代理值, 6); // 工具循环上限,防失控(默认 10)
变量 回复: 字符串 = 简单问(代理值, "用一句话介绍 qi 编程语言。");
IO.打印行(回复);
关闭代理(代理值);关闭会话Harness,不是 关闭会话 —— 这是 qi-harness 刻意保留的显式命名。代理用完先 关闭代理,会话用完再 关闭会话Harness(会话)。流式输出
想边生成边显示,用 流式问。它接受第三个参数:一个块回调,签名固定为 函数(字符串): 整数 —— 每收到一个片段就回调一次,返回 1 表示继续接收。函数返回值仍是拼好的完整文本。
导入 Harness::{ 流式问 };
导入 Harness.追踪 作为 追踪;
// 块回调:片段实时打到 stdout,返回 1 继续
函数 打印片段(片段: 字符串) : 整数 {
IO.打印(片段);
返回 1;
}
// 关掉 trace 即时打印,避免和流式片段输出交错
追踪.禁用即时打印();
变量 完整: 字符串 = 流式问(代理值, "用两句话解释什么是编译器。", 打印片段);工具调用循环
这是 qi-harness 的核心价值。你定义若干工具(每个工具 = 一个 JSON Schema + 一个处理函数),加进代理后调用 运行。如果模型决定调用工具,harness 会自动派发到你的处理函数、把结果回写进对话、再次请求模型,循环直到模型给出最终文本。模型一轮返回多个 tool_calls(parallel tool calls)时,会逐个派发各自回写。
工具处理函数签名固定为 函数(参数JSON: 字符串): 字符串,返回值必须是会被喂回模型的 JSON 字符串。
导入 标准库.字符串;
导入 Harness::{ 工具, 添加工具, 运行 };
// 工具实现:解析 args JSON,返回 JSON 结果
函数 天气查询(参数JSON: 字符串) : 字符串 {
如果 (字符串::包含(字符串::转小写(参数JSON), "beijing") == 1) {
返回 "{\"city\":\"Beijing\",\"temp\":24,\"condition\":\"多云\"}";
}
返回 "{\"error\":\"city not found\"}";
}
// 在 入口() 里:定义工具并加进代理
变量 天气工具: 工具 = (工具 {
名字: "天气查询",
描述: "查询指定城市的当前天气。",
参数schema: "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}",
处理: 天气查询,
});
代理值 = 添加工具(代理值, 天气工具);
// 运行:自动 detect tool_calls → 派发 → 回写 → 继续,直到最终文本
变量 回复: 字符串 = 运行(代理值, "北京现在天气怎样?用中文回答。");
IO.打印行(回复);设置最大步数(代理, N) 给工具循环设上限,模型若一直要求调工具不给最终答案,到达上限就停止。默认 10,调试期可设小一点。可观测与重试
追踪默认开启:每次 LLM 调用、每次工具派发都会 emit 一条 JSON 事件到 stdout。事件类型包括 llm_call / llm_response / tool_call / tool_result / error / done / stream_start / stream_chunk / stream_end。这让 Agent 像普通服务一样可以观察、记录、排查。
导入 Harness.追踪 作为 追踪;
追踪.禁用即时打印(); // 只停实时打印(和流式回调混输出时用)
追踪.禁用(); // 完全关闭追踪重试用 重试调用(策略, 代理名, 调用) 包裹任意「返回字符串的零参函数」。默认策略() 给出 3 次重试、起始 100ms、指数翻倍的退避。注意:奇语顶层命名函数不捕获局部变量,所以被包裹的「调用」要写成命名函数,通过模块级全局拿到会话。
导入 Harness::{ 重试策略, 默认策略, 重试调用, 创建代理, 简单问, 关闭代理 };
变量 全局会话: 整数 = 0; // 入口里赋值为已开的会话
// 被重试包裹的「调用」:零参,返回字符串
函数 问关键字() : 字符串 {
变量 临时代理: 代理 = 创建代理("重试助手", 全局会话);
变量 答: 字符串 = 简单问(临时代理, "qi 语言用什么关键字声明变量?");
关闭代理(临时代理);
返回 答;
}
// 在 入口() 里:
变量 策略: 重试策略 = 默认策略();
变量 回复: 字符串 = 重试调用(策略, "教程助手", 问关键字);运行说明
把上面所有片段拼成的完整示例放在仓库的 qi-harness/examples/教程_快速开始.qi。用编译器二进制运行:
$ QI_LLM_KEY=sk-... qi run qi-harness/examples/教程_快速开始.qiQI_LLM_KEY 或 DEEPSEEK_API_KEY)以及网络连接。本文写作时验证环境里只有一个无效密钥,因此不展示任何模型的对话文本 —— 那需要你自己的有效凭证才能看到,我们不会编造。不过,整个程序的结构和管线是真实验证过的:示例 qi check 通过(「所有文件语法检查通过」),并且 qi run 能编译、链接、执行到底 —— 追踪外壳真实地发出了事件。下面是真实跑出来的追踪输出(密钥无效,所以 llm_response 里是 401 鉴权错误,而不是模型回复;这恰好演示了追踪如何让失败一目了然):
$ qi run qi-harness/examples/教程_快速开始.qi
=== qi-harness 快速开始 ===
--- 一次性问答 ---
{"ts":...,"type":"llm_call","agent":"教程助手","detail":"用一句话介绍 qi 编程语言。"}
{"ts":...,"type":"llm_response","agent":"教程助手","detail":"LLM调用失败: API返回错误 401 Unauthorized: ... Authentication Fails ..."}
{"ts":...,"type":"done","agent":"教程助手","detail":"LLM调用失败: ... 401 ..."}换成有效密钥后,llm_response 的 detail 就会是模型的真实文本,工具循环会出现 tool_call / tool_result 事件对。结构不变,只是内容从错误换成真实回复。
下一步
- 还不熟悉奇语语法?先读 奇语 Qi 快速开始,把 100% 中文关键字的基础打牢。
- 想给 Agent 接上一个 HTTP 后端?看 qi-web 快速开始,用奇语写 REST API。
- 想把 Agent 封装成命令行工具?看 qi-cli 快速开始,加上子命令和参数解析。