夜雨聆风学习资料网

ARTICLE · 1044999

FastCAE AI应用助手重构(一)架构与桥接详解

FastCAE AI应用助手重构(一)架构与桥接详解

引言

FastCAE 的 AI 应用助手正在进行一次重构,目标是实现多智能体与知识图谱的接入。其核心架构为:宿主负责工程基础能力与命令执行,Python 子进程负责大模型推理与 Agent 编排,两者以一条 stdio JSONL 协议桥接,AI 通过 hostpy 反向请求真正驱动平台,危险操作再交由 HITL 人工审批。本文聚焦两件事:整体架构的层次化设计,以及 C++ 与 Python 之间的桥接关联。

一、

能力与分工

FastCAE 是一个 C++/Qt 的 CAE 平台,拥有成熟的几何、网格、求解器、后处理等能力;而当下的大模型生态几乎全部生长在 Python 里(LangChain、deepagents、MCP……)。

要让 AI 真正"用"起来,只把模型接进来是不够的,模型必须能操作平台:读数据、改模型、跑求解。于是有了一个很自然的架构取舍:

▍C++ 侧:负责 UI、组件接入、以及"真正执行命令"的平台能力;

▍Python 侧:负责大模型推理、Agent 编排、工具调用、技能加载。

两者之间用一条进程间协议连接,这就是本文的主角:stdio  JSONL桥接

▲ 双引擎分工:平台能力 × 智能生态

二、

整体架构

▲ FITKDeepAgent 整体架构全景

三、

FastCAE 组件层:FITKDeepAgent

3.1

组件接入

FITKDeepAgent 遵循 FastCAE 标准的组件工厂约定,通过 FITKComponentInterface 派生类接入:

FITKDeepAgentInterface.cpp

QString FITKDeepAgentInterface::getComponentName(){    return "FITKDeepAgent";           // 组件唯一标识,工厂据此查找}QWidgetFITKDeepAgentInterface::getWidget(const int indexPort){    Q_UNUSED(indexPort);    return new FITKDeepAgentWindow(); // 返回组件主窗口}

一句话:框架拿到组件名,创建主窗口,UI 就挂载进平台了。

3.2

桥接客户端 FITKDeepAgentClient

这是 C++ 侧桥接的核心类,职责只有一条:把 Agent.py 子进程的 stdio 协议流,转成 Qt 信号。它定义了完整的协议语义:

协议语义速览

请求(宿主 → agent):    ping / chat / resume / reset / list_sessions / shutdown事件(agent → 宿主):    pong / start / thinking / tool_call / tool_result /    answer / done / interrupt / error反向请求(agent → 宿主):    hostpy —— agent 请求平台执行命令,    宿主回写 hostpy_result(按 id 关联)

关键动作有三个:拉起子进程、逐行解析 stdout、按类型分发事件

拉起子进程——用项目自带的 venv 解释器运行 Agent.py:

startAgent() · 拉起子进程

void FITKDeepAgentClient::startAgent(){    QString pythonExe, agentScript;    if (!this->locateRuntime(pythonExe, agentScript))    {        emit sigFailed(QString("FastCAEDeepAgent runtime not found: %1").arg(_agentRoot));        return;    }    _process->setProgram(pythonExe);          // .venv/Scripts/python.exe    _process->setArguments({ agentScript });   // Agent.py    _process->setWorkingDirectory(_agentRoot); // FastCAEDeepAgent/    // 强制 UTF-8,避免 Windows 控制台默认编码破坏 JSONL 协议    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();    env.insert("PYTHONUTF8""1");    _process->setProcessEnvironment(env);    _process->start();}

这里有两个容易踩的坑,代码都处理了

①工作目录必须是 agent 根目录,否则 .env/Agent.ini、mcp_servers/、skills/ 这些相对路径都解析不到;

PYTHONUTF8=1 强制解释器以 UTF-8 输出,Windows 控制台默认 GBK 会把 JSON 里的中文搞乱。

逐行解析 stdout——一行一个 JSON,非法行直接丢弃:

slotReadStdout() · 逐行解析

void FITKDeepAgentClient::slotReadStdout(){    _process->setReadChannel(QProcess::StandardOutput);    while(_process->canReadLine())    {        const QByteArray raw = _process->readLine();        const QString line = QString::fromUtf8(raw).trimmed();        if(line.isEmpty()) continue;        QJsonParseError parseError;        QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &parseError);        if(parseError.error != QJsonParseError::NoError || !doc.isObject()) continue;        this->dispatch(doc.object());   // 合法事件交给分发器    }}

按类型分发——事件类型映射到 Qt 信号,UI 层只跟信号打交道:

dispatch() · 事件分发

void FITKDeepAgentClient::dispatch(constQJsonObject& event){    const QString type = event.value("type").toString();    if (type == "hostpy")          this->handleHostPyRequest(event);    else if (type == "pong")       emit sigConnected();    else if (type == "thinking")   emit sigThinking(event.value("content").toString());    else if (type == "tool_call")  emit sigToolCall(event.value("name").toString(),                                                    event.value("args").toObject());    else if (type == "tool_result") emit sigToolResult(event.value("name").toString(),                                                       event.value("content").toVariant().toString());    else if (type == "answer")     emit sigAnswer(event.value("content").toString());    else if (type == "done")       emit sigDone();    else if (type == "interrupt")  emit sigInterrupt(event.value("session_id").toString(),                                                     event.value("request").toObject());    else if (type == "error")      emit sigFailed(event.value("error").toString());}

四、

Python 智能体框架:FastCAEDeepAgent

Python 侧是一个基于 deepagents 0.7.13 的轻量框架,包在 fastcae_agent_framework/ 里。它被设计成一条流水线:把三类目录来源(LLM 配置、MCP 服务器、skills)组装成一个 deep agent。

4.1

入口 Agent.py

Agent.py 是整个 stdio 服务器的入口,也是"传输与引擎解耦"的体现:

Agent.py · stdio 服务器入口

async def main() -> None:    sink = StdioSink()                  # 先抓原始 stdout 的二进制 buffer    sys.stdout = sys.stderr             # 之后所有 print 诊断都去 stderr    agent = FastCAEAgent(        config_ini=_ROOT / ".env" / "Agent.ini",        llm_ini=_ROOT / ".env" / "LLM.ini",        sink=sink,        extra_tools=[build_host_tool(sink)],   # 宿主桥接工具:平台内执行命令    )    await agent.ready()    await StdioTransport(agent).run()

注意那两行关于 stdout 的处理,它是整个协议干净的关键:stdout 只承载协议 JSONL,诊断信息全部重定向到 stderr。这样宿主(C++)读取 stdout 时,永远不会被 print 日志污染。

4.2

框架门面 FastCAEAgent

FastCAEAgent 是门面类,ready() 里完成组装:

FastCAEAgent.ready() · 组装流水线

async def ready(self) -> "FastCAEAgent":    llm = LLMProxy(self.llm_ini).build(self.config.llm_section)   # 1. 模型    tools = await MCPProxy(self.config.mcp_dir).load_tools()      # 2. MCP 工具    if self.extra_tools:        tools = tools + self.extra_tools                          # 3. 宿主桥接工具    skills = SkillsProxy(self.config.skills_dir, self.config.backend_root)    interrupt_on = {name: True for name in self.config.hitl_tools} # 4. HITL 白名单    middleware = [EventMiddleware(self.sink)] if self.sink else [] # 5. 事件中间件    self.agent = create_deep_agent(        model=llm,        system_prompt=self.config.system_prompt,        tools=tools,        middleware=middleware,        checkpointer=self.checkpointer,   # InMemorySaver:会话历史存进程内存        backend=skills.backend,        skills=skills.source_paths(),        interrupt_on=interrupt_on or None,    )    return self

一个值得点出的设计:EventSink 是框架层唯一的抽象点。框架只负责把事件 emit 出来,具体怎么序列化、发给谁,完全由传输层决定。换 http、换 websocket,框架一行都不用改,只要再写一个 sink。

五、

桥接核心(一):stdio JSONL 协议

5.1

协议约定

双向都是"一行一个 JSON",UTF-8 编码。请求(宿主 → agent)共有六种:

type

字段

说明

ping

健康检查,回 pong

chat

session_id, message

发起一轮对话

resume

session_id, decisions

HITL 中断后回传人工决定

reset

session_id

清除指定会话历史

list_sessions

列出当前会话

shutdown

优雅退出

事件(agent → 宿主):start / thinking / tool_call / tool_result / answer / interrupt / done / error,每条带 timestamp 与 session_id(非空时)。

5.2

Python 侧:StdioSink

StdioSink 实现 EventSink 协议,把事件编成一行 JSON 写 stdout。它同时承担反向请求的发送与同步:

StdioSink · 事件写出与反向请求同步

class StdioSink:    def emit(self, event: dict) -> None:        with self._lock:            event = {"timestamp": _now_iso(), **event}            line = json.dumps(event, ensure_ascii=False) + "\n"            self._out.write(line.encode("utf-8"))            self._out.flush()    def request(self, payload: dict, timeout=300.0) -> dict:        """向宿主发起一次反向请求并阻塞等待应答(供工具线程调用)。"""        with self._lock:            self._reqSeq += 1            req_id = self._reqSeq            line = json.dumps({"id": req_id, **payload}, ensure_ascii=False) + "\n"            self._out.write(line.encode("utf-8"))            self._out.flush()            done = threading.Event()            holder = [None]            self._pending[req_id] = (done, holder)        if not done.wait(timeout):            return {"ok"False"error"f"host request timeout after {timeout}s"}        return holder[0]    def resolve(self, response: dict) -> bool:        """读线程收到宿主应答时调用:按 id 唤醒等待者。"""        req_id = response.get("id")        with self._lock:            slot = self._pending.pop(req_id, None)        if slot is None:            return False        slot[1][0] = response        slot[0].set()        return True

这里体现了一个精巧的并发模型:

▍emit:可能来自事件循环(chat 事件流)和工具线程(hostpy 反向请求)两个线程,内部用互斥锁保护;

反向请求用 id 关联 + threading.Event 做同步:发出请求后阻塞等待,收到 hostpy_result 时按 id 唤醒对应的等待线程。

5.3

Python 侧:StdioTransport

StdioTransport 是请求循环。关键设计:stdin 读取放在独立 OS 线程,chat / resume 经 run_coroutine_threadsafe 投回事件循环执行:

StdioTransport._dispatch() · 请求循环

def _dispatch(self, loop, sink, req) -> bool:    type_ = req.get("type")    if type_ == "hostpy_result":        sink.resolve(req)                    # 反向应答:唤醒等待的工具线程    elif type_ == "ping":        sink.emit({"type""pong""ok"True})    elif type_ == "chat":        sid = req.get("session_id"or "默认"        future = asyncio.run_coroutine_threadsafe(            self._run_turn(sid, req.get("message"or "", chat=True), loop)        future.add_done_callback(self._swallow_turn_error)    elif type_ == "resume":        sid = req.get("session_id"or "默认"        future = asyncio.run_coroutine_threadsafe(            self._run_turn(sid, req.get("decisions"or [], chat=False), loop)        future.add_done_callback(self._swallow_turn_error)    elif type_ == "reset":        self.agent.sessions.delete(req.get("session_id"))        sink.emit({"type""reset""ok"True"session_id": req.get("session_id")})    elif type_ == "list_sessions":        sink.emit({"type""list_sessions""sessions"self.agent.sessions.list_ids()})    elif type_ == "shutdown":        sink.emit({"type""shutdown""ok"True})        return True    else:        sink.emit({"type""error""ok"False"error"f"未知请求类型: {type_}"})    return False

为什么要这么绕?因为 chat 是 async 调用(MCP 会话等资源绑定在事件循环上),而 stdin 读线程不能被它阻塞——否则 chat 执行期间,hostpy_result 反向应答就没法送达,会形成双向请求的死锁。把读线程独立出来、用 run_coroutine_threadsafe 投回主循环,就同时保住了"不被阻塞"和"反向应答可达"两件事。

六、

桥接核心(二):hostpy 反向请求,让 AI 驱动平台

▲ hostpy 反向请求完整链路

6.1

Python 侧:把'执行命令'封装成工具

hostbridge.py 用 LangChain 的 @tool 装饰器,把一个"在平台内执行 Python 命令"的能力封装成工具:

hostbridge.py · 宿主桥接工具

def build_host_tool(sink, timeout=300.0):    @tool    def submit_python_command(command: str) -> str:        """Submit one Python command line to the FastCAE host platform for execution.        Use this tool to run a single Python statement inside the FastCAE        application's embedded interpreter, e.g. to drive the platform's        data model or API. Returns the evaluated result text on success, or        an error message on failure. Submit one statement per call and call        the tool repeatedly for multi-step scripts.        """        response = sink.request({"type""hostpy""command": command}, timeout=timeout)        if response.get("ok"):            return response.get("result"or "OK"        return f"ERROR: {response.get('error')}"    return submit_python_command

大模型在对话中会自主决定调用这个工具——比如用户说"把当前网格的单元数统计一下",模型就生成一条 Python 语句,交给宿主执行,再把结果写回对话。

6.2

C++ 侧:接收反向请求并执行

C++ 端收到 hostpy 事件后,走 handleHostPyRequest:

handleHostPyRequest() · 接收反向请求

voidFITKDeepAgentClient::handleHostPyRequest(const QJsonObject& request){    const QString command = request.value("command").toString();    const int requestId   = request.value("id").toInt();    // 命令经共用执行器提交宿主内嵌解释器    QString errorInfo, result;    const bool ok = FITKDeepAgentCommandExecutor::executeCommandFromText(        command, errorInfo, &result);    // 按 id 关联回写应答:成功携带 result,失败携带 error    QJsonObject response;    response.insert("type""hostpy_result");    response.insert("id", requestId);    response.insert("ok", ok);    if (ok) response.insert("result", result);    else    response.insert("error", errorInfo);    this->sendJson(response);    emit sigHostCommand(command, ok, ok ? result : errorInfo);}

6.3

真正的执行者:命令执行器

FITKDeepAgentCommandExecutor 把一行命令提交到宿主内嵌解释器。它有一个很有意思的预处理技巧——把赋值语句转成海象表达式,让 a = b 也能作为表达式求值并返回结果:

executeCommandFromText() · 命令执行器

boolFITKDeepAgentCommandExecutor::executeCommandFromText(    const QString& commandText, QString& errorInfo, QString* result){    QString statement = commandText.trimmed();    if (statement.isEmpty()) return true;   // 空命令视为成功    // 兼容赋值语句:"a = b" → "(a := b)",让赋值也能作为表达式求值返回结果    staticconst QRegularExpression assignmentPattern(        "^([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(.+)$");    const QRegularExpressionMatch m = assignmentPattern.match(statement);    if (m.hasMatch())        statement = QString("(%1 := %2)").arg(m.captured(1), m.captured(2).trimmed());    else if (statement.contains(":=")             && !(statement.startsWith('(') && statement.endsWith(')')))        statement = QString("(%1)").arg(statement);    // 提交宿主内嵌解释器(FITKPython,基于 PythonQt)    Python::FITKPythonInterface* py = Python::FITKPythonInterface::getInstance();    if (py == nullptr) { errorInfo = QObject::tr("FITKPythonInterface is null."); return false; }    QVariant pyResult;    if (!py->submit(statement, pyResult))    {        errorInfo = py->getErrorInfo();      // 优先返回解释器给出的错误        return false;    }    if (result != nullptr && pyResult.isValid() && !pyResult.isNull())        *result = pyResult.toString();    return true;}

关键在于:FITKPythonInterface 是宿主进程内嵌的解释器,它能直接访问平台注册进去的 C++ 对象和数据模型——这跟 Agent.py 子进程里的那个 venv 解释器是两码事。子进程里的 Python 没有平台上下文,只有宿主解释器能"够到"平台内部。hostpy 反向请求的意义,就是把大模型的意图,安全地送进这个有权力的解释器里执行。

补充:类注释里提到,FITKDeepAgentCommandExecutor 与 fastcaehost 原生模块共用同一实现,保证两条入口(stdio 桥接、HTTP 桥接)行为一致。

七、

HITL:把'危险操作'交给人来拍板

▲ HITL 人工审批流程

在 .env/Agent.ini 里,[hitl] tools 白名单指定需要审批的工具:

Agent.ini · HITL 白名单

[hitl]; 逗号分隔,这些工具执行前需人工审批;留空则关闭 HITLtools = submit_python_command

可以看到,宿主桥接工具 submit_python_command 默认就在审批名单里——因为让 AI 直接改平台数据是有风险的,必须人工确认。

流程是这样的:

大模型准备调用 submit_python_command,因为它命中 interrupt_on,deepagents 会中断而不是执行

FastCAEAgent._run 检测到中断,emit 一条 interrupt 事件(携带 action_requests 和 review_configs);

C++ 端 dispatch 收到 interrupt,弹出 FITKDeepAgentHitlDialog 审批框;

用户对每个待执行动作选择 approve / edit / reject / respond

决定转成 decisions 数组,经 resume 请求回传,agent 恢复执行。

C++ 窗口里对应的槽函数(节选):

slotOnInterrupt() · 审批入口

void FITKDeepAgentWindow::slotOnInterrupt(const QString& sessionId, const QJsonObject& request){    // 弹出审批对话框收集决定,取消时全部拒绝,最后 resume 回传    FITKDeepAgentHitlDialog dlg(request, this);    const QJsonArray decisions = (dlg.exec() == QDialog::Accepted)        ? dlg.getDecisions()        : /* 取消 → 全部拒绝 */;    _client->resume(sessionId, decisions);}

八、

总结

回顾一下这套设计值得称道的几个点:

进程边界即信任边界。大模型推理跑在独立 Python 子进程里,平台核心跑在 C++ 里,中间只隔一条可审计的 JSONL 协议流——AI 想动平台,只能走 hostpy 这条被审批(HITL)管住的路。

传输与引擎解耦。EventSink 是框架唯一的抽象点,Agent.py / stdio_transport.py 只是 stdio 这一种传输的应用层示例。将来换 http/websocket,框架一行不改。

一条协议流,双向请求。stdout 既承载 agent 的事件,也承载 agent 的反向请求(hostpy),用 id 关联 + threading.Event 优雅地解决了跨进程双向同步,同时用独立读线程避免了双向请求死锁。

两条入口行为一致。命令执行器 FITKDeepAgentCommandExecutor 被 stdio 桥接与 HTTP 桥接共用,平台能力只写一份。

从组件接入(FITKDeepAgentInterface)到 UI(FITKDeepAgentWindow)、桥接(FITKDeepAgentClient)、执行(FITKDeepAgentCommandExecutor),再到 Python 侧的框架流水线与 stdio 传输,整套"AI 应用助手"以一条清晰的协议缝合成一个整体:大模型负责思考,平台负责执行,人负责拍板。

了解更多

*FastCAE亮相中国工程热物理学会人工智能应用专委会成立大会暨交叉学术论坛

*FastCAE AI版:仿真软件从开发到应用,全链路AI化

*AI赋能CAE软件开发与应用技术研讨会暨FastCAE用户交流会成功举办

*北京开元工业软件研究院 关于免费开放办公空间入驻的通知

*关于公开征集“元社区”工业软件项目的通知

*FITK AI Assistant | 为 FastCAE 注入 AI 大脑

*FastCAE Agent 与 Skill 体系应用 | 面向主流AI编程工具的配置·使用·成效指南

*【FastCAE—HFEmag案例】目标体电磁散射仿真分析

*FastCAE AI 辅助开发实践|基于 Agent 与 Skill 体系的 CAE 智能化开发探索

相关学习资料