搭建OpenHarness的简单客户端
1
前言
2
OpenHarness
1)库介绍
OpenHarness(简称oh)是由香港大学HKUDS团队开发的一个轻量级基于Python的AIAgent基础设施框架,实现了完整的AgentHarness模式——即调用LLM使其成为功能性Agent所需的全部基础设施,包括工具调用、技能系统、记忆、权限治理和多Agent协调等等。
目录结构如下:
源码安装的目录如下,pip安装的有些许不同。

架构:

2)pip安装
直接通过pip安装,简单快捷:
pip install openharness-ai安装后的目录为:
C:\Python\Python312\Lib\site-packages\openharness
这种安装法与源码安装法有些模块会有些差别,但核心模块都是一样的。
3)源码安装
环境要求:
Python 3.11+Node.js 18+大模型API Key
通过下面的命令安装:
# 安装uvpip install uv -i https://mirrors.aliyun.com/pypi/simple# 克隆项目git clone https://github.com/HKUDS/OpenHarness.gitcd OpenHarness# 安装依赖,看网速了,科学的话应该会快点(做个docker镜像???)uv sync --extra dev# 启动uv run openh

4)配置模型
OpenHarness 支持三种主流供应商 API:Anthropic、OpenAI-compatible、GitHub Copilot。这里以兼容的OpenAi来配置:
打开命令行工具运行oh setup,然后选择供应商配置即可。(源码安装方式可通过PyCharm打开项目)

配置完成,再运行openh -p “你是谁”测试,成功输出表示配置完成

或者直接运行openh进入交互模式:

以上就安装完了OpenHarness这个开源库。下面来实现通过Python来与oh进行交互。
3
OpenHarness客户端
目本身oh提供了基于控制台的CLI环境,但我们是希望集成到afsim的界面里去,那么就需要创建一个类似于客户端的方式来与oh的CLI环境进行交互,主要有下面几种方式:
1)直接Python环境集成
之前的文章就是将Python环境集成到了afsim插件中,还说可以基于此来做后续扩展^_^。这种方式虽然能够解决与LLM对话的基本功能,但走到后面发现还是隔离开来比较好,一方面是集成在一起时调试是一个问题,二来需要添加很多接口,容易出错。
2)通过在oh中添加http服务器
通过在oh框架中添加一个http服务器,afsim端通过http与oh进行交互,架构如下:

这种方式我没有尝试,主要是我对Python不是很熟,还是通过C++来写更适合我。我打算从0按着开篇提到的那个Harness的实现路径来搭建一套纯C++版的,不是为了工程应用,是为了学习。
3)通过子进程调用CLI
这种方式适用于任何语言,核心交互方式是通过 CLI 命令 oh。这里主要创建一个类OpenHarnessClient。
// OpenHarnessClient.h#pragma once#include<string>#include<functional>#include<memory>#include<vector>#ifdef _WIN32#define POPEN _popen#define PCLOSE _pclose#else#define POPEN popen#define PCLOSE pclose#endif// OpenHarness 输出格式常量namespace OHOutputFormat {constexpr const char* TEXT = "text"; // 纯文本constexpr const char* JSON = "json"; // 完整 JSONconstexpr const char* STREAM_JSON = "stream-json"; // 流式 JSON 事件}// 流式事件类型(对应 --output-format stream-json)enum class OHEventType {TEXT_DELTA, // 文本增量TOOL_START, // 工具开始执行TOOL_END, // 工具执行完成TOOL_OUTPUT, // 工具输出RESULT, // 最终结果ERR, // 错误Assistant_delta, // 助手消息Assistant_complete, // 助手消息完成UNKNOWN};// 流式事件结构struct OHEvent {OHEventType type;std::string content;std::string tool_name;std::string tool_input;std::string tool_output;int tool_id = -1;};// OpenHarness 客户端class OpenHarnessClient {public:using StreamCallback = std::function<void(const OHEvent& event)>;OpenHarnessClient();~OpenHarnessClient();// 设置配置文件路径(可选)voidsetConfigPath(const std::string& path);// 设置工作目录voidsetWorkingDirectory(const std::string& cwd);// 设置权限模式voidsetPermissionMode(const std::string& mode); // default, auto, plan// 同步执行:等待完整结果后返回std::string query(const std::string& prompt,const std::string& outputFormat = OHOutputFormat::JSON);// 流式执行:实时回调每个事件voidqueryStream(const std::string& prompt, StreamCallback callback);// 取消当前执行voidcancel();private:std::string buildCommand(const std::string& prompt,const std::string& outputFormat) const;std::string parseJsonResult(const std::string& jsonOutput);voidparseStreamLine(const std::string& line, StreamCallback& callback);class Impl;std::unique_ptr<Impl> pImpl;};
// OpenHarnessClient.cpp#include"OpenHarnessClient.h"#include<cstdio>#include<array>#include<thread>#include<atomic>#include<sstream>#include<regex>#include<nlohmann/json.hpp>// 需要安装 nlohmann/json#ifdef _WIN32#include<windows.h>#else#endifusing json = nlohmann::json;class OpenHarnessClient::Impl {public:std::string configPath;std::string workingDir;std::string permissionMode = "default";std::atomic<bool> cancelled{false};std::string buildBaseCommand()const{std::string cmd = "oh";// 添加权限模式if (permissionMode == "auto") {cmd += " --permission-mode auto";} else if (permissionMode == "plan") {cmd += " --permission-mode plan";}// 添加配置文件if (!configPath.empty()) {cmd += " --settings \"" + configPath + "\"";}return cmd;}};OpenHarnessClient::OpenHarnessClient(): pImpl(std::make_unique<Impl>()) {}OpenHarnessClient::~OpenHarnessClient() = default;voidOpenHarnessClient::setConfigPath(const std::string& path){pImpl->configPath = path;}voidOpenHarnessClient::setWorkingDirectory(const std::string& cwd){pImpl->workingDir = cwd;}voidOpenHarnessClient::setPermissionMode(const std::string& mode){pImpl->permissionMode = mode;}voidOpenHarnessClient::cancel(){pImpl->cancelled = true;}std::string OpenHarnessClient::buildCommand(const std::string& prompt,const std::string& outputFormat) const {// 转义 prompt 中的特殊字符std::string escapedPrompt = prompt;std::regex quote(R"(")");escapedPrompt = std::regex_replace(escapedPrompt, quote, R"(\")");std::string cmd = pImpl->buildBaseCommand();cmd += " -p \"" + escapedPrompt + "\"";cmd += " --output-format " + outputFormat;return cmd;}std::string OpenHarnessClient::query(const std::string& prompt,const std::string& outputFormat) {pImpl->cancelled = false;std::string cmd = buildCommand(prompt, outputFormat);// 添加工作目录切换std::string fullCmd;if (!pImpl->workingDir.empty()) {#ifdef _WIN32fullCmd = "cd /d \"" + pImpl->workingDir + "\" && " + cmd;#elsefullCmd = "cd \"" + pImpl->workingDir + "\" && " + cmd;#endif} else {fullCmd = cmd;}std::array<char, 4096> buffer;std::string result;#ifdef _WIN32//隐藏一个控制台窗口,使得在之后用popen来启shell窗口的时候,不显示黑窗口,或者避免黑窗口一闪而过的情况AllocConsole(); //为调用进程分配一个新的控制台ShowWindow(GetConsoleWindow(), SW_HIDE); //隐藏自己创建的控制台#elsefullCmd = "cd \"" + pImpl->workingDir + "\" && " + cmd;#endifFILE* pipe = POPEN(fullCmd.c_str(), "r");if (!pipe) {return "Error: Failed to execute command";}while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {if (pImpl->cancelled) break;result += buffer.data();}PCLOSE(pipe);// 解析 JSON 结果if (outputFormat == OHOutputFormat::JSON) {return parseJsonResult(result);}return result;}std::string OpenHarnessClient::parseJsonResult(const std::string& jsonOutput){try {json j = json::parse(jsonOutput);if (j.contains("text")) {return j["text"].get<std::string>();} else if (j.contains("content")) {return j["content"].get<std::string>();} else if (j.contains("result")) {return j["result"].get<std::string>();}return j.dump();} catch (const std::exception& e) {// 非 JSON 格式,直接返回原文return jsonOutput;}}voidOpenHarnessClient::parseStreamLine(const std::string& line, StreamCallback& callback){if (line.empty()) return;OHEvent event;try {// stream-json 格式:每行一个 JSON 事件json j = json::parse(line);std::string type = j.value("type", "");if (type == "tool_started") {event.type = OHEventType::TOOL_START;event.tool_name = j.value("tool", j.value("tool_name", ""));event.tool_input = j.value("input", j.value("input", ""));event.tool_id = j.value("id", -1);}else if (type == "tool_completed") {event.type = OHEventType::TOOL_END;event.tool_name = j.value("tool", j.value("tool_name", ""));event.tool_output = j.value("output", j.value("output", ""));event.tool_id = j.value("id", -1);}else if (type == "error") {event.type = OHEventType::ERR;event.content = j.value("message", j.value("error", ""));}else if (type == "assistant_delta") {event.type = OHEventType::Assistant_delta;event.content = j.value("text", j.value("text", ""));}else if (type == "assistant_complete") {event.type = OHEventType::Assistant_complete;event.content = j.value("text", j.value("text", ""));}else {event.type = OHEventType::UNKNOWN;event.content = line;}}catch (const std::exception& e) {// 非 JSON 格式,作为普通文本处理event.type = OHEventType::TEXT_DELTA;event.content = line;}callback(event);}voidOpenHarnessClient::queryStream(const std::string& prompt, StreamCallback callback){pImpl->cancelled = false;std::string cmd = buildCommand(prompt, OHOutputFormat::STREAM_JSON);std::string fullCmd;if (!pImpl->workingDir.empty()) {#ifdef _WIN32fullCmd = "cd /d \"" + pImpl->workingDir + "\" && " + cmd;#elsefullCmd = "cd \"" + pImpl->workingDir + "\" && " + cmd;#endif} else {fullCmd = cmd;}std::array<char, 16384> buffer; // 更大的缓冲区std::string lineBuffer;#ifdef _WIN32//隐藏一个控制台窗口,使得在之后用popen来启shell窗口的时候,不显示黑窗口,或者避免黑窗口一闪而过的情况AllocConsole(); //为调用进程分配一个新的控制台ShowWindow(GetConsoleWindow(), SW_HIDE); //隐藏自己创建的控制台#elsefullCmd = "cd \"" + pImpl->workingDir + "\" && " + cmd;#endifFILE* pipe = POPEN(fullCmd.c_str(), "r");if (!pipe) {OHEvent errEvent;errEvent.type = OHEventType::ERR;errEvent.content = "Failed to execute command";callback(errEvent);return;}// 逐字符读取以支持实时输出(流式)char ch;while (fread(&ch, 1, 1, pipe) == 1) {if (pImpl->cancelled) break;if (ch == '\n') {if (!lineBuffer.empty()) {parseStreamLine(lineBuffer, callback);lineBuffer.clear();}} else {lineBuffer += ch;}// 刷新输出缓冲区,确保实时性fflush(stdout);}// 处理最后一行if (!lineBuffer.empty()) {parseStreamLine(lineBuffer, callback);}PCLOSE(pipe);}
然后简单的流式输出调用如下:
int main() {OpenHarnessClient client;client.setPermissionMode("default"); // 敏感操作需确认std::cout << "开始执行任务...\n" << std::endl;client.queryStream("你是谁",[](const OHEvent& event) {switch(event.type) {case OHEventType::TOOL_START:std::cout << "\n [工具开始] " << event.tool_name << std::endl;break;case OHEventType::TOOL_END:std::cout << "\n [工具完成] " << event.tool_name << std::endl;break;case OHEventType::ERR:std::cerr << "\n [错误] " << event.content << std::endl;break;case OHEventType::Assistant_delta:{// 实时输出文本,不换行QTextCodec* pCodec = QTextCodec::codecForName("gb2312");if(!pCodec) return "";QByteArray arr = pCodec->fromUnicode(event.content.c_str());std::string cstr = arr.data();std::cout << cstr;std::cout.flush();}break;case OHEventType::Assistant_complete:{// event.content 为完整内容,无需再次打印std::cerr << "\n√ [完成] " << std::endl;break;}break;default:std::cout << event.content;break;}});std::cout << "\n\n任务执行完毕" << std::endl;return 0;}
以上代码通过编译器编译后即可运行。下面将OpenHarnessClient这个类继承到warlock插件里面去,并将数据输出到界面去。
4
集成到Warlock
注:需要把OpenHarnessClient改为线程执行,不然会阻塞界面线程,造成的效果就是流式输出感觉不是流式的,会在回复完成后一次性显示出来,这是上一篇提过的。
下面是完成后的演示:
这个工具目前有下面一些问题:
1、字符编码有些问题2、界面显示也很丑^_^3、没有会话保持4、没有退出保存和启动恢复会话5、没有添加任何Memory、Skills、Tools等
基本上就是个空的oh框架通过子进程方式集成到了afsim,如果大家有需要可以下载参考参考,虽然熟悉一点Python,但只能说勉强上手,对于开发复杂度较高的项目,就会捉襟见肘,因此本文的代码插件后续不再维护,我将构建的插件打包分享给大家。

5
后记
对涉及到需要权限确认的选择;
对LLM返回的不同数据类型做不同的标注显示等;
对文件修改等等都需要进一步的优化和完善的。
然后主要的工作就是Harness本身的开发了,如Skills、MCP、Tools、SubAgent等等与智能强相关的内容,这些不是本文能讲完的,需要大家在工作中逐步开展的。
最后想说的是:对LLM与afsim相结合的摸索对于仅有闲余时间且不太熟悉Python的我来说,进度实在是比较慢,整理的内容也没什么质量^_^,最近我才醒悟过来,我没办法在这种状态下思考出LLM解决实际工程问题的方法,我的初衷是了解LLM的开发范式,因此后续会以C++的方式来构建与LLM的交互及智能体相关的内容。
往
期
推
荐

夜雨聆风