可视化服务(org.example.SemQuestServer)同时就是一套完整的 REST API:建表、管树、落位、检索全部走 HTTP,任何语言的后端都能直接调用。Java 后端可参考自带客户端示例 org.example.SemQuestClient(零依赖,拷走即用)。
- Base URL:
http://localhost:8787(server.port可改);所有请求/响应均为 UTF-8 JSON - 认证:无,且服务监听全部网卡(局域网内可直接访问)。仅本机自用无所谓;要对外/跨网段暴露,请在前面加网关做鉴权
- 并发模型:同一张表的所有请求按表级锁串行执行(不同表并行),客户端无需自己加锁;同表长判定会排队,慢模型下高并发请按表分片
- 错误统一为
{"error":"原因"},状态码:400 参数非法 / 404 资源不存在 / 405 方法不对 / 500·502 判定调用失败 - 花费标注:[模型] 会发起决策模型请求(当前后端计费),[免费] 零模型调用
mode(noul 多选分支 / choice 单选逐层)建表时定死,之后不可改
列出全部表。
出参:数组,每项
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 表 ID(tables/<id>.json) |
name |
string | 表名(全局唯一) |
mode |
string | noul | choice |
note |
string | 备注 |
createdAt |
number | 创建时间毫秒 |
demo |
boolean | 是否 demo 表(--reseed 会重播) |
docCount |
number | 落位条数 |
建表。入参 {name, mode, note?}:mode 必填 noul 或 choice;note 可选。
出参:同上单条 tableMeta。
错误:400 缺 name / mode 非法 / 表名已存在。
改表名与备注(mode 改不了)。入参 {name?, note?},只发要改的字段。
出参:改后 tableMeta。错误:404 未知表;400 空名 / 与其他表重名。
删表:内存移除 + 删除 tables/<id>.json(整棵树与落位数据)。
出参:{"ok":true,"id":"..."}。错误:404 未知表。
节点对象统一形态:{name, leaf(是否叶), other(是否兜底叶), yes?(正向判据), no?(反向判据), docCount(本节点落位条数)}。兜底叶(X_OTHER)没有判据字段。
表元信息。出参:tableMeta 全字段 + meta:{backend, mode, noulGate, choiceGate, docCount}。
全树结构(不含正文,正文走 leaf)。出参:meta 同上 + root(节点对象递归含 children,兜底叶排在子节点末位)+ tableId / name / note。
单层展开(viz 左键一层就是它)。name 缺省取 ROOT。
出参:meta + node:节点对象 + children(只含直接子节点,不带孙)。
错误:404 未知节点。
节点详情(叶与内部节点共用)。
出参:{name, leaf, other, path(根到此的节点名数组), yes?, no?, docs:[{id,content}]};内部节点另有 children 与 docTotal(子树落位去重合计——一 Doc 落多叶只计一次,与各叶直加不同)。
错误:404 未知节点 / 缺 name。
节点直取:指定节点(叶或内部节点)返回其子树内全部叶的落位内容,零模型调用。limit 可选(默认 500)、offset 可选(默认 0,翻页用),返回 docs[offset, offset+limit) 区间,未取完给 truncated:true。
出参:{node, leaves:[子树叶名 DFS 序(含兜底叶)], total(去重总数), docs:[{id, content, viaLeaves:[该文档全部落位叶名]}], truncated?}——文档按叶 DFS 顺序去重(一 Doc 多叶只出一次);兜底叶内容包含在内,viaLeaves 露出 X_OTHER 后缀可辨。
错误:400 未知节点 / 缺 node / limit 非法;405 非 GET。
增长节点 = 补层挂靠。入参 {parent, name, yes, no}:
- noul 表:
yes(正向判据)必填,no留空自动生成否定侧 - choice 表:
yes(选项描述)可空,留空用选项名本身
创建后父节点存量逐条重判回填:过门/胜出迁入新叶并挪出兜底叶。父为内部节点=补层(挂新叶+兜底叶);父为叶=分化。首个分支创建时自动生成该层兜底叶。
出参:{parent, node(新节点名), moves:[{docId, prob, moved}], judged(重判条数), moved(迁入条数)}。
错误:400 缺字段 / 未知父节点 / 节点名已存在。花费:每条存量 1 次判定请求(新节点本身不判)。
修改节点(改名/改判据),零模型调用、不重判已落位。入参 {name, newName?, yes, no}(newName 不发=不改名;改 yes/no 传新文本)。
出参:meta + {oldName, newName, parent, yes, no}。改名连带更新落位指针与兜底叶名,修改记录随快照持久化、重启自动回放。
错误:400 未知节点 / 重名 / 根节点改名等。
删除节点整个子树(含各级兜底叶),清理相关落位指针,孤儿正文从索引移除;增长/修改记录同步过滤。
出参:meta + {parent, removed(被删节点名数组), docsRemoved(随之移除的正文 ID)}。
错误:400 根节点与兜底叶不可直接删 / 末位分支受兜底落位护栏保护。
删除一条落位(零模型调用):从其全部落位叶上摘下并移出索引,此后不再被任何检索命中;树结构不动。
入参:name = 文档 ID(add 时自带的或自动生成的 U-…)。
出参:meta + {id, removedLeaves(原落位叶名), stats}。
错误:400 缺 name / 未知 ID;405 非 DELETE。
落位:一条语料沿判据树下行落进叶。入参 {content, id?}:id 缺省自动生成 U-<36进制时间戳>;同 id 重落 = 替换旧落位(先摘旧指针再落位)。
数据驱动生长(growth.enabled=true 时):本条落位全为兜底(树里没有能承接这条内容的类别,含空树根叶)→ 服务端按本条内容调生成模型提出 ≤growth.maxNew 个新分支逐个验收入树;任一收编即响应 leaves 换为新枝、记 grown(auto 标记,重启零模型回放)并挂 growthStep;全部未收编则分支当场回滚、本条留在原兜底叶数据不丢。生成模型经 growth.model 配置、提示词经 growth.prompt 配置(均可热载,提示词空白回内置模板);思考型模型可用 growth.thinking(none/low/medium/high)+ growth.maxTokens 压住输出量——该次生成占 add 总耗时九成以上,实测 qwen3.7-flash 由 38.6s 降到 5.7s(生成 3066 → 122 token)。失败原因(该层分支已满 / 缺 openrouter 凭据 / 生成调用失败)也写进 growthStep.failed。
出参:meta + {id, leaves(生长收编后为新枝), steps, growthStep?, stats}。growthStep:{parent, genTokens, genInTokens, genOutTokens, genMs, kept(收编保留数), branches:[{name, yes, prob(验收 P)}], failedBranches(已回滚分支), failed(放弃原因)}。
错误:400 缺 content / JSON 非法;502 判定调用失败(超时、上游报错)。
批量导入:docs 里每条独立落位,单条失败不牵连整批,全部结束后统一落盘一次。入参 {docs:[{id?, content}…]},单次上限 500 条(超出 400);条目缺 content 记入 failed 继续。
出参:meta + {total, added, failed:[{index, id?, error}], results:[{id, leaves, steps, growthStep?}], stats}——每条独立触发数据驱动生长(同 add,卡片挂在该条行下)。
错误:400 缺 docs / docs 空 / 超 500 条;单条判定失败进 failed 而非整体报错。
检索(纯读):自然语言查询沿判据树语义下行,命中叶上的文档按分排序返回——查不到就是查不到,不为查询改树(生长只在 add/addBatch 落兜底时触发)。节点名直取:查询与节点名完全一致时不调模型,直接返回该节点子树全部叶的落位内容(复用节点直取;fastPath:true、steps 空数组、matchedLeaves=子树叶名、score=文档在子树内的落位叶数、stats 标「节点直取 · N 条 · 零模型调用」)。noul 检索与落位同条件镜像:某层零分支过门即命中该层兜底叶(X_OTHER),其存量文档照常返回;choice 单路径,途中胜出兜底项即无命中。查询结果缓存:同一条查询(原文完全一致)再次检索时直接返回上次结果(fastPath:true,零模型调用,steps 为首次慢路轨迹的原样复放),noul / choice 均生效。缓存按命中次数排序淘汰(调用最多的条目最不易淘汰,次数最少的先出局),容量 questCacheSize 在 semquest.properties 配置(默认 256,0=关闭),保存配置热载;任何表变更(落位 / 加改删节点 / 调门控 / 提示词热载)即清空该表全部缓存,缓存仅内存不落盘。
入参 {query}。
出参:
| 字段 | 说明 |
|---|---|
query |
原查询 |
fastPath |
true = 缓存直出(零模型调用,结果与首次判定一致) |
matchedLeaves |
命中叶名(noul:过门分支,某层零分支过门时含该层兜底叶;choice:单路径末叶) |
ranked |
[{docId, score, viaLeaves(该文档命中的叶), content}];score:noul=命中叶数,choice=路径各层胜出概率连乘 |
steps |
判定轨迹(缓存直出时为首次慢路轨迹的复放) |
stats |
本次会话累计账单文本行(缓存直出零调用,不计入) |
错误:400 缺 query;500 判定调用失败。
每层一个元素:{node(层节点名), probs:{节点名: P(yes) 或各项概率}, latencyMs, inputTokens, outputTokens};choice 模式额外带 choice(胜出选项) 与 confidence(置信度)。
调门控,随表落盘,只影响其后的判定、不重排已落位。入参:noul 表 {noulGate: 0.5~1};choice 表 {choiceGate: 0~1}(发错键 400)。
出参:meta(新门控值在内)。
决策模型列表。provider 缺省=当前后端。
出参:{provider, source, current, models:[...]}。source:config(properties 手动清单优先)> upstream(上游实时拉取,openrouter 按决策模态过滤)> fallback(内置 32 款快照)。models 条目:{id, name?, free?, decisions?}(decisions:true 表示决策端点模型)。
注意:「目录在列 ≠ 端点活着」,免费款会上下线,切换后真正调用才验证。
当前后端:{backend, provider, model}。
热切换 {provider, model}:仅校验凭据非空并替换通道,不发推理。provider 可选 mock | jev | openrouter | bailian(mock 零凭据关键词匹配,体验/测试用)。
出参:{backend, provider, model}。错误:400 未知 provider / 缺凭据(key 是否有效在真正判定时才暴露,401=key 错、403=无额度)。
自我生长开关/生成模型有效值 + openrouter chat 模型清单(生成模型目前仅支持 openrouter):{enabled, model, hasKey, source, models}。有效值 = viz 🌱 运行时覆盖优先,回退 growth.enabled / growth.model 配置(热载);hasKey = openrouter 凭据是否已配。models 清单:growth.models 手动清单(config)> 上游 /api/v1/models 零 token 公开元数据(upstream,免费款排前)> 静态候选(fallback);当前生效模型缺席时置顶补入,条目 {id, name?, free}。
运行时热改 {enabled?, model?}(至少一项):替换内存覆盖,零推理,立即生效;重启回 growth.* 配置值。两字段均可选,仅出现的字段被覆盖(enabled 须为布尔,字符串等错型报 400)。
出参:{enabled, model, hasKey}。错误:400 两字段全缺 / enabled 非布尔。
# 建表 → 加分支 → 批量导入 → 检索(同款查询第二次起缓存直出,零模型调用)
curl -s http://localhost:8787/api/tables -d '{"name":"售后问题","mode":"noul","note":"演示"}'
curl -s http://localhost:8787/api/t/<id>/node -d '{"parent":"ROOT","name":"退款","yes":"用户明确要求退钱退款"}'
curl -s http://localhost:8787/api/t/<id>/addBatch -d '{"docs":[{"id":"Z-001","content":"我要退钱,不然投诉你们"},{"content":"物流十天下单还没发货"}]}'
curl -s http://localhost:8787/api/t/<id>/quest -d '{"query":"有没有人要求退款的"}'零依赖 Java 调用见 org.example.SemQuestClient(连同 Json.java 两个文件拷进你的后端即可),main() 里有从建表到检索的完整演示。