2026年10月15日 · 阅读 —

问数应用架构怎样落到代码:可信上下文、任务API、状态与缓存

Agent 与 Skills测试与评测

问数应用架构怎样落到代码:可信上下文、任务API、状态与缓存

专用问数的目标架构需要回答四件事:谁有权发起任务,模型产生的候选怎样成为合法查询,结果能交给谁,失败之后从什么状态恢复。模块图、业务API和状态机应共同描述这些规则。

下图为框架中立的建议架构,尚未实现。SDK 路线用普通应用代码表达显式工作流,LangGraph 路线用节点与边表达;身份、授权与取数边界均由应用和数据平台强制执行。

模块必须掌握的规则框架能帮什么不能交给模型的内容
企微接入/身份验证当前渠道所用的消息来源与服务鉴权;账号映射;重复请求处理接入工作流可信 tenant/userid,不能从问句、工具参数或客户端自报认定
会话与存储绑定租户、用户、会话、请求;读取时鉴权;并发和删除策略状态传递、检查点thread_id 只是索引,不是授权;共享记忆按业务权限读取
指标与查询契约指标口径、时间端点、币种、维度、数据水位和版本约束结构化输出业务语义正确性和授权范围
查询服务与数仓真实权限、允许的表/列/函数、只读、配额、审计与超时作为一个窄工具调用数据凭据、账号兜底及权限过滤修改权
结果与交付结果核验、最小化、附件权限、群聊接收范围和链接有效期组织回答流程数值事实和文件访问边界
运维与恢复部署、锁版本、预算、取消、重试、备份和回滚流程恢复能力业务幂等、权限撤销和外部副作用的交付语义

userid 不写死在生产逻辑里,也不让模型填写。 由接入层验证后作为不可被模型覆盖的服务上下文传递。测试时可以在隔离环境绑定固定测试账号,但必须证明这个绑定没有跳过身份和权限检查。Pydantic AI 的依赖注入可辅助传递服务上下文,鉴权仍由应用实现。

查询路线与框架路线分开决定:沿用现有 Text-to-SQL+Query/Result/Answer 契约和独立 Oracle。服务校验 SQL 语法树、允许对象及数据授权;单纯检查 SELECT 关键字不够。语法树也不能代替数仓行列权限。受控 IR(模型填写受限查询参数,服务编译 SQL)仍是后续小指标试点,迁移编排不等于立即重写全部 SQL。

LangGraph persistence区分会话检查点与跨会话存储。生产须选持久存储并配置访问隔离。interrupts 文档说明恢复时节点会重执行。因此查询、附件生成和消息发送需要请求编号、去重及幂等设计;不能把检查点当成“只执行一次”的保证。

E.1 模块化应用先行,按真实隔离要求拆部署

建议初期保持少量可独立理解的业务模块,不先建设多 Agent 平台或一组微服务。以下为职责示意,不是当前代码目录或已实现模块。

智能问数应用
  渠道与 API | 企微适配、身份验证、请求和响应契约
  查询工作流 | 阶段顺序、澄清、预算、状态与取消
  业务契约与语义 | 指标口径、版本、合法查询对象
  模型适配 | Agent SDK、模型接口、结构化候选和解释
  授权与查询 | 实时权限、SQL校验、受限执行、数据审计
  会话与结果 | 用户归属、缓存、保留、附件访问
  交付与观测 | 消息状态、关联证据、脱敏日志
独立评测
  agent-eval-harness | 真实Target适配、独立Oracle、回归证据

依赖方向应明确:工作流依赖业务契约与受控服务;SDK 适配依赖 SDK;业务契约不引用 SDK 的会话或工具对象。API 返回公司定义的结果,不直接暴露第三方 run 对象。实现上先用少量普通函数和明确的数据类型即可,不为每个模块增加抽象工厂或插件系统。

代码模块职责不是凭据安全约束。 如果模型工具能够执行任意代码,共享进程中的秘密仍可能被访问。查询凭据需要不可达时,应放在独立受限执行服务,通过认证接口访问,并限制网络出口;业务入口禁止注册任意代码、终端或文件操作工具。具体部署隔离根据威胁模型和数据平台要求验证。

其他模块先同部署也可以;当独立扩缩容、数据网络隔离或发布责任确有需要时再拆分。这样同时减少运行时耦合与自研系统的部署复杂度。

E.2 三条信任分界与不可破坏的规则

信任分界输入如何被看待强制执行位置必须保留的证据
用户/渠道到应用问句、session_id、附件均是可修改输入;渠道身份也须验证来源接入层鉴权、企业账号映射和会话所有权检查验证主体、渠道消息编号、请求编号;不记录密钥
模型到业务执行模型生成的 SQL、工具参数和文本只是候选业务契约、预算、授权与查询服务;模型不能覆盖身份或策略候选摘要、语义版本、校验结果、策略版本和允许对象
结果到接收人已经查到的数据仍须按接收范围交付;缓存和附件也是数据出口结果/交付层、缓存读取、附件下载和服务端访问控制结果版本、交付对象、当前授权决定与交付状态

身份认证回答“调用者是谁”,授权回答“这次能访问什么”。前者通过不代表后者通过。模型收到的 Schema、指标描述和检索片段,也应按可见范围裁剪,避免在真正查数前泄露元数据或敏感业务信息。

行列权限至少需要明确到数据集、字段与组织范围。聚合查询还需业务定义哪些统计可以披露;合法读取底层行并不自动证明所有小样本汇总都可发布。不要通过简单拼接 WHERE 实现全部 SQL 权限,要在受控查询与数据平台层验证连接、子查询、函数和过滤语义。

执行前检查一次;长时间澄清或暂停后恢复要重新检查;交付和下载也要检查。如果权限源不可用,不能回退到管理员账号或此前缓存的宽权限。只有隔离环境明确配置的测试替身才可使用模拟授权,生产路径不得悄悄降级。

群聊是单独交付策略:提问者获授权,不代表群内成员都能看到同一数据。业务未定义合法群聊披露范围时,应按既定策略拒绝在群内交付敏感结果或转到经验证的个人入口;不能让模型自行决定。

E.3 业务 API 应围绕查询任务设计

以下是拟议接口契约,不是当前部署已经提供的 API。名字和路径供设计讨论,正式实现再按现有接口规范确定。

拟议接口业务输入与行为权限/状态约束测试要点
POST /api/v1/query-requests问句、业务会话引用、幂等键;创建查询任务身份从可信认证上下文取得;先验证会话归属;不接受客户端覆盖执行账号伪造身份、重复请求、跨用户session、非法范围
GET /api/v1/query-requests/{id}读取任务状态、允许查看的结果与错误任务所属用户/组织校验;当前授权仍适用;请求编号不是访问凭据枚举他人编号、撤权后读结果、状态一致性
POST /api/v1/query-requests/{id}/clarifications提交对同一任务的业务澄清只允许合法等待状态;重新鉴权;不扩大原权限重复回复、已结束任务、并发澄清与恢复
POST /api/v1/query-requests/{id}/cancel请求取消在途查询先核对任务归属;区分已请求取消与执行器确认取消取消/完成竞态、远端查询仍运行、重复取消

提交接口不暴露通用工具列表、Shell 命令或任意执行脚本。传入业务问题不等于允许原始 SQL;若未来确需专家 SQL 入口,要单独授权和校验,仍走同一执行约束。

响应至少区分 request_id、trace_id、业务状态、结构化错误、可见结果及口径/数据版本。API 版本、指标版本和 SDK 版本各自管理;换 SDK 时不要求客户端改变业务字段。HTTP 传输是否成功与查询业务是否成功分开表达。

业务错误需区分“需要澄清”“不支持该任务”“权限拒绝”“候选校验失败”“上游不可用”“预算耗尽”和“查询结果为空”。这些状态对应不同用户行动与运维处理,不能都压成模型生成的一段自然语言。错误详情还需控制可见范围,避免泄露内部表名、凭据或其他用户信息。

测试接口不是生产后门:固定账号和数据注入只能存在于隔离测试配置,测试主体依然经过对应鉴权逻辑。生产不增加任意 userid 的冒充参数;日志查看和调试数据读取也须有权限。

E.4 可恢复的执行状态与有限重试

图为正常流程和一个取消分支。各阶段都应有失败出口;权限拒绝、非法查询和范围外任务结束流程,不进入无限“反思后再试”。每个阶段记录当前状态、版本与结果引用,敏感 SQL 和数据仅在受保护的证据存储中保留。

应用负责总超时、模型轮次、工具调用次数、查询配额与重试预算。SDK 和 HTTP 客户端自身的重试也纳入这个预算,避免外层与内层重复放大请求。口径歧义可以澄清;错误 SQL 可在明确限制内修正;权限拒绝不能通过修改问句不断试探。

网络超时不一定代表查询没有执行。执行器需要查询编号、状态查询及取消能力;无法确定远端状态时记录为待核实状态,而不是自动新建同一查询。客户端收到取消确认前,不宣称数据库任务已停止。

相同幂等键应绑定调用主体、规范化请求和业务版本;重复请求若载荷不同必须拒绝或按明确契约处理。检查点、查询记录和消息发送没有天然跨系统事务,重启后要根据已执行事实恢复,不能简单从头双跑。消息交付使用持久待发送记录和去重;具体一次性效果由渠道能力与恢复验证决定,不承诺框架自动保证。

E.5 语义、缓存与观测纳入业务架构

指标定义需要明确口径、粒度、时间区间、时区、币种和可查询维度,并绑定版本。数据水位表示数据何时完整就绪。模型选对了字段却使用过期口径或不完整数据,同样可能得到业务错误的答案。

结果缓存必须区分调用主体/授权范围、查询条件、指标版本、数据版本或水位。命中缓存仍要验证当前权限;权限变更后失效或拒绝。不能只按问句文本共享缓存,也不能让被裁掉的字段继续留在可下载文件中。

从渠道消息到模型、授权、实际查询与交付使用可关联的编号。常规监控保留时延、调用次数、预算、错误类别、版本和脱敏摘要;敏感原始证据单独控制权限和保留期限。SDK Trace 辅助排障,查询服务和数仓审计证明实际执行,独立评测证明是否满足业务契约。

flowchart TB
 A[企微与业务API] --> B[认证及会话归属]
 B --> C[应用任务与状态]
 C --> D[模型或SDK产生候选]
 D --> E[业务契约及预算校验]
 E --> F[当前授权与查询服务]
 F --> G[受限数据平台]
 G --> H[结果及事实校验]
 H --> I[回答图表与文件]
 I --> J[交付权限及实际发送]
 C -.-> K[独立观测与评测]
 F -.-> K
 J -.-> K

把架构判断落实为可验证的状态机

采用 SDK 以后,应用需要拥有任务状态。至少要区分已接收、执行中、查询完成、产物生成、已交付、取消请求中、已取消和失败。数据库已经完成查询但消息发送失败,应该保留查询完成及发送失败的事实,恢复时重试交付;不能把整个任务重新当成一条全新查询。

下面按照任务状态语义给出一个教学实现,只演示状态迁移校验,不是内部业务源码。真实系统还需要数据库事务、版本比较更新及事件日志。示例全部在内存执行,断言中的非法迁移应被拒绝。

TRANSITIONS = {
    "accepted": {"running", "cancelled"},
    "running": {"query_done", "cancel_requested", "failed"},
    "query_done": {"rendered", "failed"},
    "rendered": {"delivered", "failed"},
    "cancel_requested": {"cancelled", "query_done", "failed"},
    "cancelled": set(),
    "delivered": set(),
    "failed": set(),
}

def transition(state, event):
    if event not in TRANSITIONS.get(state, set()):
        raise ValueError(f"invalid transition: {state} -> {event}")
    return event

assert transition("running", "query_done") == "query_done"
assert transition("running", "cancel_requested") != "cancelled"
assert transition("cancel_requested", "query_done") == "query_done"
try:
    transition("accepted", "delivered")
except ValueError:
    pass
else:
    raise AssertionError("查询未执行就交付,应被拒绝")

取消请求存在竞争:请求到达时查询可能已经完成。因此允许记录查询完成,但接下来是否生成和交付,应由产品的取消政策决定,不能据此继续默认发送。这个例子只验证状态顺序,不证明数据库取消或消息去重已实现。生产代码需要把查询编号和取消确认状态一同保留,才能区分请求发出与实际停止。

架构要求可执行评测动作需要读回的状态
身份不能由模型改写在用户问题和候选条件里注入另一账号实际查询主体仍等于认证主体
查询和授权绑定在授权通过后撤销范围,再继续查询执行前采用批准的授权时效策略
取消不伪装完成暂停查询,提交取消,再恢复网络取消请求、查询终态、交付状态分别可查
交付有幂等语义同一产物发送步骤重试多次业务交付标识及实际发送次数
SDK可被替换固定相同候选条件替换模型实现业务API、授权与结果契约保持一致
任务可独立复测固定条件和结果,分别注入查询与发送失败错误阶段明确,恢复不会覆盖旧证据

这些检查可以接入 agent-eval-harness:Runner 负责执行固定输入;环境读回查询与消息状态;领域 Oracle 检查表中的约束;框架保留每次执行的证据和版本。模型输出的“已取消”“已发送”都只是候选文字,不能替代环境读回。

缓存和幂等,需要同时带上主体与版本

缓存结果是否可复用,至少依赖主体或批准的共享策略、授权版本、语义版本、数据水位和查询条件。只用问题文本做键,同一句“上个月销售多少”可能在不同用户之间复用。使用会话编号也不充分,同一会话暂停期间权限仍然可能变化。

以下代码演示受控缓存的一个保守策略:版本必须全部一致,结果包含的门店还要属于当前允许范围。它同样是教学实现,并未提供真实缓存存储或授权服务。每次调用的 current 都应来自可信服务,不能由问题正文提供。

from dataclasses import dataclass

@dataclass(frozen=True)
class QueryContext:
    subject: str
    policy_version: str
    semantic_version: str
    data_version: str
    query_digest: str


def reusable(saved, current, result_scope, allowed_scope):
    return saved == current and set(result_scope) <= set(allowed_scope)

before = QueryContext("tester_a", "p1", "m1", "s1", "q1")
assert reusable(before, before, {"A"}, {"A", "B"})
after_revoke = QueryContext("tester_a", "p2", "m1", "s1", "q1")
assert not reusable(before, after_revoke, {"A"}, {"B"})
other_user = QueryContext("tester_b", "p1", "m1", "s1", "q1")
assert not reusable(before, other_user, {"A"}, {"A", "B"})
assert not reusable(before, before, {"A", "C"}, {"A", "B"})

同样的方法用于交付幂等标识:标识应关联业务任务、主体、产物版本和交付动作,而不是每次重试都生成新值。进程内集合可以演示去重,无法承诺进程崩溃后的恰好一次。需要测试“发送已发生但回执丢失”的情形,核对发送服务是否支持幂等键、是否能查询已有消息;这些能力缺失时,应采用可审计的补偿和人工处理策略。

把状态、授权和交付规则掌握在应用中,选型就有了可执行的比较方法。对 Hermes 和 SDK 路线使用相同状态反例、缓存撤权题和交付重试题,再比较各自需要多少适配、观察和维护。框架名称本身不会给出这个答案。

状态与错误也需要指标定义

业务状态一致率表示跨任务存储、查询执行器和交付记录中,适用状态关系符合契约的任务比例。取消请求确认率与实际停止率不同:前者说明请求被接收,后者需要远端执行终态。重试次数统计所有层级,SDK、HTTP客户端和应用外层的重试不能分别忽略后只看总耗时。

幂等以业务动作及标识为单位验证。查询重试、文件生成和消息发送的允许次数可能不同,不能一律要求所有底层调用只有一次。对于“发送已发生、回执丢失”,先按消息标识读回事实;渠道没有查询或幂等能力时,记录明确的不确定状态与补偿政策,不宣称应用代码自动获得恰好一次。

缓存命中率也不是正确性指标。还要看授权版本、语义版本、数据水位及查询条件是否匹配;缓存对象缺少来源范围时,即使是合计数字也不能忽略授权核验。未能确认这些条件时,宁可不复用该缓存,也不能用一次命中改善性能数字。

经验感想

应用掌握可信上下文、状态和数据出口,模型负责生成候选。模块可以先同部署,但凭据隔离、当前授权与外部副作用需要按实际威胁和执行事实验证。

推荐标签

#智能问数 #Agent评测 #测试工程 #技术设计