graphify 增量更新与重聚类实战:--update / --cluster-only 机制与源码原理解析
本文是 graphify 知识图谱项目面向 AI 编码助手(Droid 等平台)的 Skill 参考文档的技术解读。该文档(源码位于 graphify/skills/droid/references/update.md,其由工具生成的验证副本位于 tools/skillgen/expected/graphify__skills__droid__references__update.md)专门描述两个高频维护命令 --update(增量重抽取)与 --cluster-only(仅重聚类)的完整执行协议。读完本文,你将掌握 graphify 在"新增/修改/删除文件之后如何低成本地保持知识图谱新鲜"的全部关键步骤、中间产物契约、脚本级可复制命令,以及隐藏在命令背后的 Manifest 双哈希、replace-on-re-extract 合并等底层设计原理。
这份参考在什么时机被加载
文档开宗明义:本参考只在用户传入了 --update 或 --cluster-only 时才被读取,首次全量构建从不加载它。 这是 graphify Skill 体系里"按需加载、绝不预先膨胀上下文"的典型设计——主技能文档(例如 graphify/skill-droid.md)描述了从零构建的完整 Steps 1–9 流水线,而本参考是它的两个"后续维护分支"的专用操作手册:
--update:针对上次运行之后新增/修改/删除文件的增量场景,只重新抽取变更文件,节省 token 与时间;--cluster-only:跳过抽取,直接对已有图重新做社区聚类并刷新全部产物。
两份文件同属一套 per-platform 参考集(在 graphify/skills/droid/references/ 目录下与 transcribe.md、query.md、exports.md 等并列),因此文中出现的 Steps 编号(如 Step 3A/3B/3C、Steps 4–8)均指向主技能文档的流水线阶段。
--update:增量重抽取的执行协议
--update 的核心思想是"只对发生变化的文件重做工作"。整个流程由四类状态文件串联:.graphify_incremental.json(探测结果)、.graphify_detect.json(下游步骤共享的语料状态)、.graphify_extract.json(抽取/合并结果)、.graphify_manifest.json(历史基线)。
第一步:用 detect_incremental 探测变更
在确认用户传了 --update 后,先执行下面的探测脚本,它会调用 graphify/detect.py 中的 detect_incremental() 并写出 .graphify_incremental.json:
$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.detect import detect_incremental, save_manifest
from pathlib import Path
result = detect_incremental(Path('INPUT_PATH'))
new_total = result.get('new_total', 0)
print(json.dumps(result, indent=2, ensure_ascii=False))
Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\")
deleted = list(result.get('deleted_files', []))
if new_total == 0 and not deleted:
print('No files changed since last run. Nothing to update.')
raise SystemExit(0)
if deleted:
print(f'{len(deleted)} deleted file(s) to prune.')
if new_total > 0:
print(f'{new_total} new/changed file(s) to re-extract.')
"
该脚本末尾已经实现了空变更短路:当 new_total == 0 且没有删除文件时,打印提示并以退出码 0 提前结束,避免下游空转。
detect_incremental() 的返回结构(源码位于 graphify/detect.py)值得逐字段理解,后续每一步都要消费它:
| 字段 | 含义 | 驱动哪些后续步骤 |
|---|---|---|
files |
当前全量语料,按文件类型分组(code/document/paper/image/video…) | 写入 .graphify_detect.json 的 all_files,供需要全语料上下文的步骤使用 |
new_files |
本次"变更子集",同样按类型分组 | 驱动 Step 3A(AST)+ Step 3B0(语义缓存命中检查)只处理变更文件 |
new_total |
变更文件总数 | 短路判断与提示 |
deleted_files |
从磁盘上消失的 manifest 行 | 后续作为 prune_sources 的唯一来源 |
excluded_files |
仍存活但被排除规则移出扫描的文件 | 与删除严格区分,绝不当作删除去 prune |
incremental |
恒为 True,标记这是一次增量运行 | 状态语义标记 |
需要澄清其变更判定机制:detect_incremental() 并不是每次都全量计算内容。快路径:mtime 未变且对应 hash 匹配 → 直接判为"未变更"(除一次 stat 外零磁盘 IO);慢路径:mtime 变化后再用 MD5 与对应 hash 字段比对,才决定是否真正重抽。同时它内部对 manifest 中消失的行做了"删除 vs 被排除"的二分(按磁盘上文件是否还存在),这是避免把 .gitignore/.graphifyignore/--exclude 改动误判为删除的关键。更细节地,文档中提到 kind="semantic"(graphify extract 默认)对比 semantic_hash,而 --update(AST-only)需要 kind="ast" 对比 ast_hash,以保证"AST 更新过的文件在语义抽取时会被重新排队"。
第二步:回填 .graphify_detect.json
探测完成后,必须把增量状态"翻译"成全量流水线各步骤无条件读取的 .graphify_detect.json,使 Step 3A–6 在增量运行时看到正确的状态:
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
Path('graphify-out/.graphify_detect.json').write_text(json.dumps({
'files': r.get('new_files', {}),
'all_files': r.get('files', {}),
'total_files': r.get('new_total', 0),
'total_words': r.get('total_words', 0),
'skipped_sensitive': r.get('skipped_sensitive', []),
'needs_graph': True,
}, ensure_ascii=False), encoding=\"utf-8\")
"
这里体现了一个核心语义约定:files 携带的是变更子集(让 Step 3A 的 AST 与 Step 3B0 的缓存检查只碰变更文件);all_files 携带完整语料(供任何需要全语料上下文的步骤使用)。 二者分工不同,缺一不可。
第三步:判断"纯代码变更"以选择流水线分支
在新文件存在的前提下,先判断这次变更是否全部是代码文件。文档提供了扩展名白名单集合,几乎覆盖 graphify AST 支持的主流语言后缀(.py/.ts/.js/.go/.rs/.java/.cpp/.c/.rb/.swift/.kt/.cs/.scala/.php/.cc/.cxx/.hpp/.h/.kts/.lua/.toc,以及 Fortran 全家族 .f/.F/.f90/.F90/.f95/.F95/.f03/.F03/.f08/.F08):
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-8').read()) if Path('graphify-out/.graphify_incremental.json').exists() else {}
code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc','.f','.F','.f90','.F90','.f95','.F95','.f03','.F03','.f08','.F08'}
new_files = result.get('new_files', {})
all_changed = [f for files in new_files.values() for f in files]
code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed)
print('code_only:', code_only)
"
依据 code_only 布尔值,流程分化出两条路径:
路径 A —— code_only 为 True(纯代码变更):完全不需要 LLM。 打印提示 [graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed),只对变更文件执行 Step 3A(确定性 AST 抽取),整体跳过 Step 3B(不派发任何语义子代理),随后直接进入下面的 merge 合并,再照常执行 Steps 4–8。这正是增量更新省 token 的主要来源——改几行代码无需为它们再跑一次大模型语义抽取。
路径 B —— code_only 为 False(变更里混入了文档/论文/图片/视频)。 需要走完整的 Steps 3A–3C 语义流水线,并且有一个前置陷阱必须处理:如果变更里有视频(new_files['video'] 非空),必须先按 graphify/skills/droid/references/transcribe.md(即 Step 2.5)对它们做语音转写,然后重写 .graphify_detect.json,把转写出的文本路径并入 files['document']、删掉 files['video']。文档特别指出:否则原始的 .mp4/.mp3 路径会被当作不可读媒体直接喂给语义子代理(#1392)。
路径 C —— 没有新文件,只有删除。 此时需要先创建一个空抽取文件,让 merge 步骤有能力"剪除"被删文件的遗留节点:
if [ ! -f graphify-out/.graphify_extract.json ]; then
echo '[graphify update] Only deletions -- creating empty extraction for merge.'
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
"
fi
第四步:build_merge 合并新旧图谱
无论走哪条路径,最终都要通过 build_merge()(源码位于 graphify/build.py)把"本次新抽取"与磁盘上的 graph.json 合并。文档给出了完整脚本,注释里浓缩了大量血泪教训:
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from graphify.build import build_merge
from graphify.detect import save_manifest
# Load new extraction and incremental state
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', []))
# prune_sources is ONLY for genuinely DELETED files. Changed/re-extracted files are
# handled by build_merge's replace-on-re-extract (#1344): every source_file in
# new_chunks is dropped from the base before merge, so old/stale nodes don't survive.
# Do NOT add `changed` here: with root= passed, prune_set relativizes to the same base
# as the freshly merged nodes and would DELETE the re-extracted content (#1178 is moot
# now that replace — not the dedup pass — reconciles changed files).
prune = list(deleted) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801).
# Pass root= so prune_sources (absolute paths from detect_incremental) are
# relativized to match the graph's relative source_file values; without it
# nothing is pruned and stale nodes accumulate on every update (#1361).
# directed=IS_DIRECTED: replace IS_DIRECTED with True if --directed was given, else
# False. Without it a --directed --update silently rebuilds undirected and collapses
# reciprocal A<->B edges (#1392).
G = build_merge(
[new_extraction],
graph_path='graphify-out/graph.json',
prune_sources=prune,
root='INPUT_PATH',
directed=IS_DIRECTED,
)
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
# Write merged result back to .graphify_extract.json so Step 4 sees the full graph
merged_out = {
'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)],
'edges': [
# Explicit source/target last so they win over any stale attrs in d.
{**{k: val for k, val in d.items() if k not in ('_src', '_tgt', 'source', 'target')},
'source': d.get('_src', u), 'target': d.get('_tgt', v)}
for u, v, d in G.edges(data=True)
],
# G.graph[\"hyperedges\"] holds hyperedges from both existing graph.json
# and new_extraction (build_merge combines them). Falling back to
# new_extraction only would silently drop prior-run hyperedges (#801).
'hyperedges': list(G.graph.get('hyperedges', [])),
'input_tokens': new_extraction.get('input_tokens', 0),
'output_tokens': new_extraction.get('output_tokens', 0),
}
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged_out, ensure_ascii=False), encoding=\"utf-8\")
print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)')
# Save manifest so next --update diffs against today's state, not the
# prior run's baseline (prevents ghost-node reports on subsequent updates).
# root= matches the build_merge call above so the manifest keys stay relative to
# the scan root — portable across clones/machines, so --update keeps matching
# cached files instead of missing every one after a move (#1417).
#
# Only stamp semantic files (docs/papers/images) that ACTUALLY produced output
# THIS run (new_extraction is this run's fresh extraction, read above before the
# merge overwrote the file): a changed doc whose chunk failed must stay unstamped
# so the next --update re-queues it, otherwise it is marked done and its content
# is lost forever (#2015). Mirrors the library extract path
# (cli._stamped_manifest_files + clear_semantic + scan_corpus).
from graphify.cli import _stamped_manifest_files
_manifest_files = _stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH'))
# Changed semantic files dispatched this run but NOT stamped had their chunk fail
# or be omitted; clear any stale semantic_hash so they are re-queued (#1948).
_sem_types = ('document', 'paper', 'image')
_dispatched = {f for t, fl in incremental.get('new_files', {}).items() if t in _sem_types for f in fl}
_stamped = {f for fl in _manifest_files.values() for f in fl}
_cleared = _dispatched - _stamped
# scan_corpus = the RAW full corpus so in-root files newly excluded since last run
# are dropped rather than masquerading as deletions; untouched rows preserved (#1908).
_scan = {f for fl in incremental['files'].values() for f in fl}
save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
print('[graphify update] Manifest saved.')
"
围绕这段脚本,有三组设计要点必须理解到位,否则极容易在生产中埋坑:
1. prune_sources 只装"真删除"文件。 被修改/被重抽的文件由 build_merge 的 replace-on-re-extract 语义处理——凡出现在 new_chunks 里的 source_file,合并前会先从既有基础图中按层剔掉其旧节点/旧边,再并入新内容,因此陈旧节点不会残存(#1344)。代码注释明确警告:绝不把 changed 加进 prune_set——一旦传了 root=,prune 集会被相对化到与刚合并节点相同的基础路径上,等于把刚重抽出来的内容亲手删掉。
2. root= 与 directed= 两个参数缺一不可。 root= 使 detect_incremental 返回的绝对路径 prune 源被相对化为与图中 source_file 一致的形式,否则"什么都剪不掉",每次更新都会堆积幽灵节点(#1361)。directed= 必须按用户当初构建时是否传了 --directed 显式传入 True/False:若漏传,一次 --directed --update 会静默把有向图重建为无向图,把互为引用的 A↔B 双向边折叠掉(#1392)。对照源码可见,build_merge 的 directed=None 缺省行为是继承磁盘上既有图的 directed 标志(graphify/build.py),所以脚本中的显式传参属于"不依赖图文件自描述、把决定权握在调用方"的稳妥写法。
3. Manifest 写入是三重语义的叠加。 脚本先从 cli._stamped_manifest_files() 构造"只含本run真正产出过的语义文件"的清单,然后 save_manifest(源码见 graphify/detect.py)一次性落实三个守卫:
root=相对化(#1417):manifest 键以相对扫描根的形式落盘,可跨机器/克隆位置移植,--update在仓库挪动后仍能命中缓存而非全量失效;- 只 stamp 真正产出的语义文件(#2015):某份变更文档若本 run 的分块抽取失败,就必须保持未 stamp 状态,让下一次
--update重新排队——否则会被当作"已完成"而永久丢失内容; clear_semantic(#1948):本 run 派发过但未 stamp 的语义文件,其旧的semantic_hash必须被清空(集合差_dispatched - _stamped),避免掩蔽抽取遗漏;同时scan_corpus必须传原始完整语料(incremental['files']),让自上次以来新被排除在根内文件被正确丢弃而不是伪装成删除,且未触碰行原样保留(#1908)。
配合说明 _stamped_manifest_files()(源码位于 graphify/cli.py)的判定细节:节点与超边(hyperedges)都算有效语义产出可触发 stamp(#1920),而"仅产出边的结果"因无法在图里形成实体表示,必须保持未 stamp 以便下次重排(#2927);出现在 partial_source_files 中的文件即便有产出也只是截断片段,同样不 stamp(#933 同机制)。
合并完成后打印节点/边总数,随后 照常运行 Steps 4–8(社区聚类、标注、报告生成等),把合并后的完整图交给后续分析阶段。
第五步:展示图差异(graph diff)
在 Step 4 完成之后,更新文档要求向用户展示这次增量前后的图谱差异。实现依赖 graphify/analyze.py 的 graph_diff():它对比新旧两个 NetworkX 图的节点集合、边集合(有向图按 (u, v, relation) 成键、无向图按排序后的端点成键),返回 new_nodes、removed_nodes、new_edges、removed_edges 与一段人类可读的 summary(如 "3 new nodes, 5 new edges, 1 node removed")。执行脚本如下:
$(cat graphify-out/.graphify_python) -c "
import json
from graphify.analyze import graph_diff
from graphify.build import build_from_json
from networkx.readwrite import json_graph
import networkx as nx
from pathlib import Path
# Load old graph (before update) from backup written before merge
old_data = json.loads(Path('graphify-out/.graphify_old.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_old.json').exists() else None
new_extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
G_new = build_from_json(new_extract, directed=IS_DIRECTED)
if old_data:
G_old = json_graph.node_link_graph(old_data, edges='links')
diff = graph_diff(G_old, G_new)
print(diff['summary'])
if diff['new_nodes']:
print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5]))
if diff['new_edges']:
print('New edges:', len(diff['new_edges']))
"
脚本依赖一个"合并前备份"约定,文档要求严格执行:merge 之前先 cp graphify-out/graph.json graphify-out/.graphify_old.json,展示完 diff 后再 rm -f graphify-out/.graphify_old.json 清理,避免备份残留在输出目录里污染下一次运行。
--cluster-only:仅对既有图重聚类
当用户没有改动任何语料、只想重新做社区发现时(例如调整了聚类分辨率、换了标签后端、或发现上一轮社区划分不理想),使用 --cluster-only。其语义被文档概括为三个字:自包含(self-contained)——跳过 Steps 1–3 的所有抽取工作,只对现存 graph.json 重新聚类并刷新全套产物:
graphify cluster-only .
该命令执行后会自动完成:重新社区聚类 → 重新命名社区 → 依据既有图重新生成 GRAPH_REPORT.md、graph.json 与 graph.html。
与 --update 相关的三点必须严格遵守:
- 不要重跑 Steps 5–9。这些步骤会读取中间文件(
.graphify_extract.json、.graphify_detect.json、.graphify_analysis.json),而一次完整构建的 Step 9 清理阶段早已把它们删掉,重跑只会触发FileNotFoundError(#1392)。 - 命令完成后,照常把刷新后的
GRAPH_REPORT.md摘要呈现给用户。 - 它从零开始读取
graph.json自身携带的directed标志(参见 graphify/cli.py 的build_from_json(_raw, directed=_directed)),再进入cluster()流程。
从源码(graphify/cli.py)可以看到,graphify cluster-only 比表面看起来更"重"也更"稳":它加载既有图后依次执行 cluster()(默认分辨率 --resolution=1.0)、通过 remap_communities_to_previous() 把新社区 ID 映射回旧社区(按节点重叠度匹配,保证已保存的社区标签不错位,#1027)、计算社区内聚度、检测 god nodes 与跨社区意外连接,再统一落盘。可用参数一并列出:
| 参数 | 作用 |
|---|---|
graphify cluster-only <path> |
指定项目路径(缺省为当前目录) |
--graph <path> |
指向其他位置/项目租户的 graph.json |
--resolution <float> |
Leiden 聚类分辨率,默认 1.0 |
--exclude-hubs <percentile> |
聚类前剔除高度数 hub 节点 |
--no-label |
只生成 "Community N" 占位名,不调用 LLM |
--backend <name> / --model <name> |
重命名社区的 LLM 后端与模型 |
--no-viz |
跳过可视化产物 |
--missing-only / --min-community-size |
标签补齐策略与最小社区规模 |
从源码看增量设计的三个底层机制
1. Manifest 双哈希:ast_hash 与 semantic_hash 各司其职
detect_incremental 的变更判定之所以又快又准,靠的是 graphify/detect.py 中每行 manifest 记录的 {mtime, ast_hash, semantic_hash, seen}。两个 hash 字段对应两条不同频度的抽取链:--update(AST-only)消费 ast_hash,graphify extract(语义抽取)消费 semantic_hash;save_manifest 通过 kind="ast"/"semantic"/"both" 决定 stamp 哪个。此外 save_manifest 还处理了若干防御性边界——mtime 相同文件系统时钟粒度内的"等长改写"会触发一次 MD5 兜底验证(_mtime_may_hide_a_rewrite)、legacy 纯数值 mtime 与 {mtime, hash} 旧格式的向后兼容、以及写盘前用原子写避免崩溃留下半截 manifest。
2. build_merge 的分层替换语义(AST 层与语义层共存)
build_merge 中每个文件在图上存在两类生产者:确定性 AST 抽取层与语义/LLM 抽取层,两层节点集共存在同一张图里(COEXIST,#2333/#2336)。因此它的替换是按层(tier)作用域的——重抽语义层绝不能误删该文件的 AST 层贡献。源码用 _is_ast_tier(n) 判定节点归属层后分别构造 new_ast_sources 与 new_sem_sources,只在对应的源集合内做"先剔旧再并入"(graphify/build.py)。这也回答了文档里"为什么改文件靠 replace 而非 dedup 调和(#1178 已过时)"的历史疑问:build() 时代的同名合并只折叠完全重复的边,节点一旦从新版本消失就会永远残留,build_merge 的 replace 语义从根上解决了累积污染。
3. Hyperedge 在增量链路中的"携带"契约
脚本把超边单独处理并非多此一举。合并结果写回 .graphify_extract.json 时,hyperedges 必须取自 G.graph['hyperedges']——因为 build_merge 已经把既有 graph.json 与 new_extraction 的超边合并进了图的 graph 属性;若回退到只读 new_extraction 的超边,会静默丢掉历史 run 的全部超边(#801)。这与 _stamped_manifest_files 把 hyperedge 计为有效语义产出(#1920)互为表里:超边是 graphify 里"3 个以上节点共享一个概念"的一等实体,增量更新的每一条链路都必须对它负责。
实战易错清单与建议
综合文档中的 issue 注释(#801/#1027/#1178/#1344/#1361/#1392/#1417/#1908/#1948/#2015/#2333 等)与源码实现,把最常见的坑归纳如下,供实现或审查增量流程时逐条对照:
- 把
changed文件混入prune_sources:在root=相对化后会误删刚重抽出的内容;删除集合必须只来自deleted_files。 - 漏传
root=:绝对路径的 prune 源永远匹配不上图中的相对source_file,结果是每次更新都在累积陈旧节点(#1361)。 - 漏传
directed=:--directed --update会静默退化为无向重建并折叠双向边(#1392)。 - 视频文件未经转写直接进入语义流水线:原始
.mp4/.mp3路径会被当作不可读媒体派发给子代理;必须先走 graphify/skills/droid/references/transcribe.md 的 Step 2.5 并把转录文本并入files['document']。 --cluster-only之后重跑 Steps 5–9:中间文件已被 Step 9 清理,必然抛FileNotFoundError。- 变更文档抽取失败却仍被 stamp:会永久丢失其内容;必须保持未 stamp 让下次
--update重新排队,或借clear_semantic主动清空(#1948/#2015)。 save_manifest传过滤后的子集而非原始语料作scan_corpus:会误删仅因过滤被省略的行;应传原始 detect 输出,让"新增排除的文件"与"真删除"保持可区分(#1908)。- diff 展示前忘记备份旧图、展示后忘记清理:
.graphify_old.json备份与删除必须成对出现。
小结
--update 与 --cluster-only 构成了 graphify 全量流水线之外最常用的两条维护路径:前者用"Manifest 双哈希差分 → 纯代码短路 → replace-on-re-extract 合并 → 精确 manifest 回写"把每次变更的增量成本压到只重抽真正变化的内容;后者则在零抽取的前提下让社区划分可以反复重算。理解这两条路径,本质上是理解 graphify 状态机的中枢——.graphify_* 中间文件、Manifest 行、graph.json 三者的读写契约。若希望进一步深入相邻主题,可继续阅读同目录下的 add-watch.md(监听模式)、query.md(查询协议)与 exports.md(导出能力)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00