internal-context-query-spec.md 14 KB

内部上下文查询接口规范(Internal Context Query API Spec)

版本:v1.1 最后更新:2026-08-13 对应实现:backend/src/main/java/com/agent/management/engine/NodeContextQueryService.java


1. 概述

1.1 设计目标

为工作流中每个节点的 LLM 调用提供"按需查询前驱节点信息"的能力:

查询类型 工具名(Python 端) 用途
路径图 get_predecessor_paths 当前 visit 的所有前驱访问链(含智能回边)
输出查询 get_node_output 某个前驱节点的 envelope.data 或单字段
中间数据 get_node_meta 某个前驱节点的思考过程 / 工具调用 / TODO 列表

1.2 核心约束

  • 快:路径图与 visit 列表查询命中内存索引,亚毫秒级
  • 准:按 sortOrder 严格有序,visitUuid 单点精确路由
  • 稳:内存优先(运行中 + 结束后 30 分钟),DB 回退兜底
  • 安全:一次性 token 鉴权 + runId 强校验,token 与 visitUuid 绑定

1.3 与 External Workflow API 的区别

维度 External API (/api/v1/...) Internal Context Query (/internal/context/...)
调用方 业务方前端 / 第三方集成 Hermes Bridge 子进程内的 LLM 工具调用
鉴权 /api/v1 Token(业务方提供) 一次性 token(Java 自动生成)
路径前缀 /api/v1/... /internal/context/...
用途 业务方观察 / 控制运行 LLM 在节点执行时按需查询前驱上下文

2. 接口列表

所有接口均为 POST 方法,请求体为 JSON,响应体统一为 Result<T> 结构:

{
  "code": 200,
  "message": "success",
  "data": <T>
}

错误码:401(token 无效或过期)、403(runId 不匹配)、404(visit 未找到)、503(功能已禁用)。

2.1 POST /internal/context/path

用途:查询当前 visit 的所有前驱访问链。

请求体:

{
  "runId": "abc12345",
  "visitUuid": "550e8400-e29b-41d4-a716-446655440000"
}

响应:

{
  "code": 200,
  "message": "success",
  "data": {
    "visitUuids": ["v1", "v2", "v3", "v4"],
    "nodeIds": ["A", "B", "C", "G"],
    "paths": [
      {
        "sequence": ["A", "B", "C", "G"],
        "visitUuids": ["v1", "v2", "v3", "v4"],
        "hasBacktrack": false
      }
    ],
    "backtrackEdgeKeys": []
  }
}

含智能回边的响应(G 飞回到 C 第二次访问):

{
  "data": {
    "visitUuids": ["v1", "v2", "v3", "v4"],
    "nodeIds": ["A", "B", "G", "C"],
    "paths": [
      {
        "sequence": ["A", "B", "G", "C"],
        "visitUuids": ["v1", "v2", "v3", "v4"],
        "hasBacktrack": true
      }
    ],
    "backtrackEdgeKeys": ["G->C"]
  }
}

2.2 POST /internal/context/output

用途:查询某个前驱节点的输出(envelope.data 整包或单字段)。

请求体:

{
  "runId": "abc12345",
  "nodeId": "B",
  "field": null,
  "iterSeq": null
}

字段说明:

  • field:为 null 返回整个 data;指定字段名返回单字段值
  • iterSeq:为 null 取最后一次访问;指定 1/2/3 取对应访问

响应(envelope 形式):

{
  "data": {
    "nodeId": "B",
    "iterSeq": 1,
    "visitUuid": "v2",
    "status": 200,
    "message": "调用成功",
    "data": {
      "result": "hello",
      "count": 10
    },
    "requestedField": null
  }
}

响应(单字段):

{
  "data": {
    "nodeId": "B",
    "iterSeq": 1,
    "visitUuid": "v2",
    "status": 200,
    "message": "调用成功",
    "data": "hello",
    "requestedField": "result"
  }
}

2.3 POST /internal/context/meta

用途:查询某个前驱节点的中间数据(思考过程 / 工具调用 / TODO)。

请求体:

{
  "runId": "abc12345",
  "visitUuid": null,
  "nodeId": "B",
  "kind": "THINKING",
  "iterSeq": null
}

字段说明:

  • visitUuid:非空时优先使用,忽略 nodeId
  • nodeId:visitUuid 为空时使用
  • kind:THINKING / TOOL_CALLS / TODO / ALL;为空按 ALL 处理
  • iterSeq:同 output 接口

响应(THINKING):

{
  "data": {
    "nodeId": "B",
    "iterSeq": 1,
    "visitUuid": "v2",
    "thinking": "第一步\n第二步"
  }
}

响应(TOOL_CALLS):

{
  "data": {
    "nodeId": "B",
    "iterSeq": 1,
    "visitUuid": "v2",
    "toolCalls": [
      {"tool": "search", "args": "{q:1}"},
      {"tool": "search", "result": "结果摘要"},
      {"tool": "calc", "args": "{x:2}"}
    ]
  }
}

响应(TODO,取最后一次 TODO_UPDATE):

{
  "data": {
    "nodeId": "B",
    "iterSeq": 1,
    "visitUuid": "v2",
    "todoList": [
      {"text": "任务A", "status": "completed"},
      {"text": "任务B", "status": "pending"}
    ]
  }
}

响应(ALL,含原始 logs):

{
  "data": {
    "nodeId": "B",
    "iterSeq": 1,
    "visitUuid": "v2",
    "thinking": "...",
    "toolCalls": [...],
    "todoList": [...],
    "logs": [
      {"type": "THINKING", "timestamp": "...", "message": "...", "detail": null},
      {"type": "TOOL_CALL", "timestamp": "...", "message": "...", "detail": "..."}
    ]
  }
}

3. Token 鉴权

3.1 生命周期

[节点执行开始]
    ↓
HermesAgentExecutor.execute 调用 InternalContextTokenService.generateToken(runId, visitUuid)
    ↓
token 通过 HermesBridgeClient.run 的 context_query 字段传给 Python
    ↓
LLM 在一次 visit 内多次工具调用复用同一 token(TTL 5 分钟)
    ↓
[节点执行结束(成功 / 失败)]
    ↓
InternalContextTokenService.invalidate(token) 立即失效

3.2 请求头

X-Context-Token: <48 字符 hex 字符串>

3.3 安全语义

  • token 不参与 URL,仅放 Header,避免日志泄露
  • token 与 visitUuid 绑定,跨 visit 不可复用
  • 同一 runId+visitUuid 同时存活 token 上限 4(防御 bug)
  • TTL 5 分钟,超时自动清理
  • runId 必须与 token 关联的 runId 一致,否则返回 403

4. 配置项

application.yml 中 hermes.context-query.*:

键 默认值 说明
enabled true 是否启用内部上下文查询接口
token-ttl-seconds 300 token 有效期(秒)
cleanup-interval-seconds 60 后台清理过期 token 间隔(秒)
max-tokens-per-visit 4 单次 runId+visitUuid 同时存活 token 上限

4.1 降级开关语义

hermes.context-query.enabled=false 时:

  1. Java 端:/internal/context/** 三个接口一律返回 503
  2. Bridge 协议:HermesAgentExecutor 不生成 token,HermesBridgeClient 不在 body 加 context_query 字段,Prompt 中不注入工具说明
  3. Python 端:未收到 context_query 字段时不注册 3 个工具,LLM 不会感知到该能力

降级后工作流执行不受影响,仅 LLM 失去"按需查询前驱上下文"的能力。


5. Python 端实现指引

Python hermes_bridge.py 需要在 /run 接口接收 context_query 字段,并注册 3 个工具。

5.1 RunRequest 扩展

class RunRequest(BaseModel):
    # ... 现有字段 ...
    context_query: Optional[dict] = None  # {base_url, token, run_id, visit_uuid}

5.2 工具注册(伪代码)

def _register_context_query_tools(agent, context_query: dict):
    """根据 context_query 配置注册 3 个上下文查询工具"""
    base_url = context_query["base_url"]
    token = context_query["token"]
    run_id = context_query["run_id"]
    visit_uuid = context_query["visit_uuid"]

    headers = {"X-Context-Token": token, "Content-Type": "application/json"}

    def get_predecessor_paths() -> dict:
        """获取当前节点的所有前驱访问路径"""
        resp = requests.post(
            f"{base_url}/internal/context/path",
            headers=headers,
            json={"runId": run_id, "visitUuid": visit_uuid},
            timeout=10,
        )
        return resp.json()["data"]

    def get_node_output(node_id: str, field: Optional[str] = None,
                        iter_seq: Optional[int] = None) -> dict:
        """获取前驱节点的输出(整包或单字段)"""
        resp = requests.post(
            f"{base_url}/internal/context/output",
            headers=headers,
            json={"runId": run_id, "nodeId": node_id,
                  "field": field, "iterSeq": iter_seq},
            timeout=10,
        )
        return resp.json()["data"]

    def get_node_meta(node_id: Optional[str] = None, visit_uuid: Optional[str] = None,
                      kind: str = "ALL", iter_seq: Optional[int] = None) -> dict:
        """获取前驱节点的中间数据(思考/工具/TODO)"""
        resp = requests.post(
            f"{base_url}/internal/context/meta",
            headers=headers,
            json={"runId": run_id, "visitUuid": visit_uuid, "nodeId": node_id,
                  "kind": kind, "iterSeq": iter_seq},
            timeout=10,
        )
        return resp.json()["data"]

    agent.register_tool(get_predecessor_paths)
    agent.register_tool(get_node_output)
    agent.register_tool(get_node_meta)

5.3 在 run_agent 中调用

@app.post("/run")
async def run_agent(req: RunRequest):
    agent = _get_or_create_agent(...)
    if req.context_query:
        _register_context_query_tools(agent, req.context_query)
    # ... 后续逻辑 ...

6. 性能基线

查询场景 延迟(p99) 说明
路径图(内存命中) < 5ms 50 个 visit 以内
路径图(DB 回退) < 50ms 索引在 run_id + sort_order
输出查询(内存命中) < 3ms 直接 Map.get
输出查询(DB 回退) < 30ms visit_uuid unique 索引
中间数据(ALL,内存) < 10ms 含 logs 序列化
中间数据(DB 回退) < 50ms 含 logs_json 解析

7. 引擎集成点

引擎类 集成内容
AgenticExecutor scheduleNode 入口:visitIndex.register + tokenService.generateToken
WorkflowLevelExecutor 同上(DAG 模式)
HermesAgentExecutor / HermesSmartActionExecutor 调 HermesBridgeClient.run 时构造 ContextQueryConfig
HermesBridgeClient body 加 context_query,Prompt 加工具说明(已完成)
NodeContextVisitIndex appendLog 在 SSE 事件回调中触发;appendOutput 在节点完成时触发;markRunFinished 在运行结束时触发

8. 相关文档

  • 实施计划:docs/plans/node-context-query-tool-plan.md
  • External API 规范:docs/specs/external-workflow-api-spec.md
  • AGENTIC 工作流计划:docs/plans/agentic-workflow-plan.md

9. 安全注意事项(Python Bridge 端)

本节面向 Hermes Bridge 子进程的维护者。token 在 Java 侧已通过短 TTL + 显式 invalidate + per-visit 限流 三重防护, 但 Python 端如果使用不当仍可能成为泄漏点。以下为强制约束。

9.1 Token 存储

  • 禁止写入磁盘:token 不得写入日志文件、Hermes Home 目录、调试 dump 文件等任何持久化媒介。
  • 进程内存生命周期:token 仅保存在工具调用的闭包/参数中,工具返回后立即让 GC 回收,不要放进模块级全局变量。
  • 禁止打印:异常 traceback、debug 打印等一律不得输出 token 原文,需要时仅打印前 4 字符 + ***。

9.2 HTTP 请求构造

  • 仅允许 localhost:base URL 由 Java 侧通过 context_query.base_url 传入,Python 端不得修改。 Bridge 启动时若发现 base_url 指向非 127.0.0.1/localhost,应拒绝注册工具并记录告警。
  • 强制 HTTPS 头校验:响应中若出现 Set-Cookie、Location 等敏感头,工具应直接丢弃响应并返回错误。
  • 超时硬上限:单次请求超时 ≤ 5s,避免 LLM 工具调用长时间挂起。
  • 重试限制:失败后不重试或最多 1 次(指数退避),避免被诱导产生大量请求。

9.3 工具响应处理

  • token 不回显:响应体不得包含 token 字段。Java 侧返回的 runId、visitUuid 也仅作为调试上下文短时使用, 不得在 LLM 可见 prompt 中重复输出(避免被 Prompt Injection 窃取)。
  • 输出长度限制:单字段返回值长度上限 100KB,超过时截断并附 truncated: true 标记,防止恶意节点构造超大输出 导致 LLM 上下文爆炸。
  • 路径数截断感知:get_predecessor_paths 返回的 paths 数组若达到 100 条上限,工具描述应提示 「结果可能被截断」,引导 LLM 用更具体的 nodeId 重查。

9.4 Prompt Injection 防御

  • 查询返回内容仅作为 LLM 输入:不直接拼接到 system prompt,也不作为代码执行(如 eval、exec)。
  • JSON 字段白名单:解析返回 JSON 时仅读取预定义字段(如 data.output、data.thinking), 其余字段一律忽略,避免恶意前驱节点通过特殊字段名触发 Bridge 端异常行为。
  • 工具调用频率监控:Bridge 应记录单次 visit 的工具调用次数,超过 50 次(可调)时拒绝后续调用并返回错误, 防御恶意 LLM 通过高频查询拖垮 Java 侧。

9.5 Java 侧防线(已实现,供 Python 端对照参考)

防线 实现 说明
Token TTL hermes.context-query.token-ttl-seconds: 120 默认 2 分钟,覆盖单节点常规时长
Token 显式失效 HermesExecutorSupport.invalidateToken finally 块 visit 结束立即移除,不留 TTL 尾巴
Token 限流 maxTokensPerVisit: 4 单 visit 同时存活 token 上限,防御 bug 反复生成
runId 匹配校验 InternalContextQueryController.authenticate token 绑定的 runId 必须与请求体 runId 一致
接口降级开关 hermes.context-query.enabled: false 一键禁用,所有 endpoint 返回 503
内存索引上限 NodeContextVisitIndex.MAX_VISITS_PER_RUN = 200 防御 Planner 死循环导致内存爆炸
路径深度上限 WorkflowPathResolver.MAX_PATH_DEPTH = 200 防御极端深 DAG 导致栈溢出

10. 变更历史

版本 日期 变更
v1.1 2026-08-13 初始版本:三类查询接口 + token 鉴权 + 内存优先/DB 回退
v1.1.1 2026-08-13 安全加固:TTL 120s、VisitIndex 内存上限、路径深度限制、Python 端安全章节