乐于分享
好东西不私藏

AFSim_Ai应用_搭建OpenHarness的简单客户端

AFSim_Ai应用_搭建OpenHarness的简单客户端

搭建OpenHarness的简单客户端

1

 前言

经前段时间写了一篇怎么在wizard插件里面集成Python执行环境并与大模型进行简单对话的文章《大模型对话Python环境集成》。后来研究Harness时看到Github上介绍Claude Code的框架设计从0通过Python实现其核心模块的系列文章(Learn Claude Code)大受启发。然后经过周老师的提醒,在本地安装了OpenHarness来研究与afsim的结合。
对于学习来说,沿着(Learn Claude Code)一步步实现Harness是最好的,因为实现的都是最核心的内容,能更深入的理解其设计思想,精简的才能更好的来理解,不必被一些暂时用不到的功能分散注意力。
本文做的是开发一个warlock客户端通过子进程调用CLI的方式来集成OpenHarness输出到界面的,请往下看。

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";           // 完整 JSON    constexpr 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 _WIN32        fullCmd = "cd /d \"" + pImpl->workingDir + "\" && " + cmd;#else        fullCmd = "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);    //隐藏自己创建的控制台#else    fullCmd = "cd \"" + pImpl->workingDir + "\" && " + cmd;#endif    FILE* 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 _WIN32        fullCmd = "cd /d \"" + pImpl->workingDir + "\" && " + cmd;#else        fullCmd = "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);    //隐藏自己创建的控制台#else    fullCmd = "cd \"" + pImpl->workingDir + "\" && " + cmd;#endif    FILE* 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

理论上应该是搭建为wsf_plugin的,这样就能在后台引擎中来驱动,而不是与warlock绑定。我这里为了偷懒就直接放到warlock去了,这样既可以直接调用afsim的api,也能有个界面可以直接显示^_^,因为界面只是输入和输出不涉及核心逻辑,要剥离出来放到wsf_plugin里面也比较容易,要增加的只是界面与wsf_plugin的数据传输,其他的逻辑基本不用怎么大改,再议^_^,请查看《天线方向图实时绘制插件》文章来搭建warlock插件。
这里界面我直接沿用了上一篇的界面就两个QTextEdit,一个输入,一个输出,用LLM自动就能搭出来。将上面的OpenHarnessClient的实现文件添加到插件源码中。界面点击发送按钮时将用户输入发送到OpenHarnessClient的实例,并实时接收回复并展示即可。

注:需要把OpenHarnessClient改为线程执行,不然会阻塞界面线程,造成的效果就是流式输出感觉不是流式的,会在回复完成后一次性显示出来,这是上一篇提过的。

下面是完成后的演示:

已关注
关注
重播 分享 赞

这个工具目前有下面一些问题:

1、字符编码有些问题2、界面显示也很丑^_^3、没有会话保持4、没有退出保存和启动恢复会话5、没有添加任何Memory、Skills、Tools等

基本上就是个空的oh框架通过子进程方式集成到了afsim,如果大家有需要可以下载参考参考,虽然熟悉一点Python,但只能说勉强上手,对于开发复杂度较高的项目,就会捉襟见肘,因此本文的代码插件后续不再维护,我将构建的插件打包分享给大家。

5

后记

上面实现了OpenHarness这个开源库通过子进程调用的方式集成CLI输出到warlok界面,但这只是基本的集成工作,还需要针对不同的消息做一些界面显示上的修改,比如:

对涉及到需要权限确认的选择;

对LLM返回的不同数据类型做不同的标注显示等;

对文件修改等等都需要进一步的优化和完善的。

然后主要的工作就是Harness本身的开发了,如Skills、MCP、Tools、SubAgent等等与智能强相关的内容,这些不是本文能讲完的,需要大家在工作中逐步开展的。

最后想说的是:对LLM与afsim相结合的摸索对于仅有闲余时间且不太熟悉Python的我来说,进度实在是比较慢,整理的内容也没什么质量^_^,最近我才醒悟过来,我没办法在这种状态下思考出LLM解决实际工程问题的方法,我的初衷是了解LLM的开发范式,因此后续会以C++的方式来构建与LLM的交互及智能体相关的内容。

往

期

推

荐

外部数据统一接入分发中间件

服务端仿真引擎框架

天线方向图实时绘制插件

wkf界面开发和插件扩展

相关学习资料