Files
context-gatekeeper/test_100rounds_4topics.py

372 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
100轮4话题对照实验验证上下文门控器的话题隔离与召回能力
"""
import sys
sys.path.insert(0, '/root/.openclaw/workspace/context-gatekeeper')
from src.gatekeeper import ContextGatekeeper
# ============================================================
# 实验设计
# ============================================================
# 4个话题每话题25轮交替提问总计100轮
#
# 话题1Redis 分布式锁 + 缓存策略
# 话题2Python asyncio 并发编程
# 话题3PostgreSQL 查询优化
# 话题4Git 工作流与分支管理
#
# 验证维度:
# 1. 话题隔离问话题4时前3个话题不被召回
# 2. 召回完整话题4的相关内容被完整覆盖
# ============================================================
GATE = ContextGatekeeper(token_budget=4000)
# ---- 话题1Redis轮次 1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 97----
TOPIC1_ROUNDS = list(range(1, 100, 4)) # [1, 5, 9, ..., 97]
TOPIC1_PAIRS = [
("Redis 分布式锁和 RedLock 算法有什么区别?",
"RedLock 是对单 Redis 实例分布式锁的增强,通过多数节点加锁来提高可靠性,但实现复杂且性能较低。普通分布式锁依赖单个 Redis 主从master 故障时可能丢锁。"),
("Redis 集群环境下怎么做分布式锁?",
"可以用 RedLock 算法在多个独立 Redis 实例上加锁,或者用 Redisson 的 RLock支持 Redis Cluster 自动续期。看你的业务对一致性的要求有多高。"),
("Redis 惰性删除和定期删除有什么区别?",
"惰性删除是查询时检查 key 是否过期过期就删CPU 友好但可能堆积过期 key。定期删除是每隔一段时间扫一批 key删掉过期的会吃 CPU但更及时。"),
("Redis 的过期 key 对 RDB 快照有什么影响?",
"执行 SAVE 或 BGSAVE 时,已过期的 key 不会被写入 RDB 文件。但主从复制时,从库会收到完整的 key 包括过期的,依赖从库自身惰性删除清理。"),
("Redis 主从复制断线后如何增量同步?",
"Redis 2.8 之后支持增量同步,从库断线重连后发送 PSYNC 命令并附上 master_replid 和 offsetmaster 只发送差异部分,比全量同步快很多。"),
("Redis 的 Lua 脚本有什么应用场景?",
"主要用 Lua 脚本来做原子操作,比如多个 key 的读写需要原子性、分布式锁的加锁检查过期一体化、执行一段复杂的过滤逻辑。 EVALSHA 可以缓存脚本避免每次传输。"),
("Redis GeoHash 在附近的人功能里怎么用的?",
"GeoHash 将经纬度编码为字符串,用 ZADD 存位置GEORADIUS 或 GEOSEARCH 查附近。原理是把地球划成格子,编码前缀相同的点物理上就相近,适合粗筛。"),
("Redis 的大 key 问题怎么排查和处理?",
"用 --bigkeys 参数扫描,用 SCAN 遍历 keys 统计大小。处理方案:拆分成 hash 结构STRING 转 zset/list对齐冷热数据定期压缩。"),
("缓存穿透、击穿、雪崩分别是什么?",
"穿透是查一个不存在的 key 直接打到 DB击穿是热点 key 过期瞬间大量请求打到 DB雪崩是大量 key 同时过期或 Redis 宕机。布隆过滤器、空值缓存、加随机 TTL 可以分别应对。"),
("Redis Cluster 的槽迁移过程是怎样的?",
"迁入节点 MIGRATE 目标 key 到迁出节点 D客户端收到 MOVED 重定向后请求新节点。迁移中访问旧槽会返回 ASK 转向ASK 不更新本地槽映射所以不是 MOVED。"),
("Redis 和 Memcached 的核心区别是什么?",
"Memcached 是纯内存 KV只支持 STRING 类型。Redis 支持多种数据结构hash/zset/list/set有持久化选项性能稍低但功能丰富很多。"),
("Redis LRU 缓存淘汰策略怎么配置的?",
"通过 maxmemory-policy 设置allkeys-lru 对所有 key 淘汰volatile-lru 只淘汰带 TTL 的。LRU 算法是采样式的,在 accuracy 和 performance 之间折中。"),
("Redis Pipeline 和事务的区别是什么?",
"Pipeline 是客户端批量发命令,减少 RTT 但不保证原子性。事务用 MULTI/EXEC原子执行但 WATCH 可以做乐观锁。Lua 脚本是更强大的原子方案。"),
("Redis 慢查询日志怎么分析?",
"用 SLOWLOG GET 查看最近慢查询记录,看 latency 列和 command 列。重点关注 O(N) 以上命令如 KEYS、SMEMBERS、HGETALL及时加索引或改写。"),
("Redis 的发布订阅有什么缺点?",
"发布订阅是无状态的,消息不持久化,订阅者离线期间的消息直接丢弃。如果需要可靠消息队列,用 Stream 结构替代 pub/sub。"),
("Redis Cluster 为什么用 16384 个槽?",
"16384 = 2^14节点间心跳包用 bitmap 标记自己负责的槽1.6KB 可以表示全部槽位,信息量适中。主从复制心跳包频率和这个数字也有关系。"),
("Redis 哨兵模式下主节点故障切换流程是什么?",
"哨兵监控主节点主观下线, quorum 哨兵投票认定客观下线后发起选举,得票最多的哨兵负责执行切换,发送 SLAVEOF NO ONE 成为新主,其他从节点指向新主。"),
("Redis ZSet 的实现为什么用跳表而不是 B+树?",
"跳表实现简单,插入/删除/查询都是 O(log n)范围查询也高效。Redis 作者觉得 B+ 树实现复杂且顺序操作性能对内存不友好。"),
("Redis 内存碎片怎么产生的,怎么处理?",
"内存碎片来自 key 写入删除的反复、大小分配策略、64字节对齐。可以用 MEMORY PURGE 触发内存整理或者重启节点。4.0+ 有自动整理选项。"),
("Redis 数据类型和应用场景怎么对应?",
"STRING 存缓存和计数器HASH 存对象LIST 存队列和最新N条SET 存标签和去重ZSET 存排行榜BITMAP 存签到和实时统计STREAM 可靠消息。"),
("Redis 加锁后服务挂了导致锁无法释放怎么办?",
"加锁时设置 value 为唯一标识(如 UUID释放前先检查 value 是否匹配,匹配才 DEL。或者用 Redisson 的看门狗机制自动续期。"),
("Redis 如何实现延迟队列?",
"用 ZSet 把任务执行时间戳作为 score轮询 ZRANGEBYSCORE 取当前时间之前的任务,执行后移除。或者用 Stream 的 XREADGROUP 配合无人认领消息的 IDLE TIME。"),
("Redis 客户端分片怎么做,有什么优缺点?",
"客户端分片在应用层计算 key 落在哪个节点,比如 CRC16(key) % slot数。优点是不用代理延迟低缺点是扩缩容需要手动迁移数据并改配置。"),
("Redis Cluster 的最大限制是什么?",
"每个 key 默认只有一个 slot不支持跨 slot 的事务和 Lua 脚本(需要用 hashtag 指定 keys 在同一槽)。最大 16384 个槽,官方建议最多 1000 节点。"),
]
# ---- 话题2Python asyncio轮次 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 74, 78, 82, 86, 90, 94, 98----
TOPIC2_PAIRS = [
("Python asyncio 里 await 后面可以接什么?",
"await 后面必须接一个 awaitable 对象比如协程async def 的返回值、Task、Future 对象。普通函数不行,必须用 asyncio.create_task() 包装。"),
("asyncio.Task 和 Future 的区别是什么?",
"Future 是一个低级的可等待对象代表一个异步操作的最终结果。Task 是 Future 的子类,专门用来调度协程执行,内置了状态管理和结果获取。"),
("asyncio.gather 和 asyncio.wait 的区别是什么?",
"gather 会等所有任务完成返回结果列表如果任一任务异常会立即传播。wait 会返回 (done, pending) 元组,不会主动抛出异常,更灵活。"),
("Python 异步编程里怎么避免回调地狱?",
"用 async/await 语法把嵌套回调展平成链式调用,配合 asyncio.create_task() 并发执行多个异步操作。写起来和同步代码风格接近。"),
("asyncio 的事件循环是怎么工作的?",
"事件循环不断从队列里取协程和回调执行,遇到 await 就暂停当前协程,切换到其他就绪的协程,直到所有协程都完成或遇到 I/O 阻塞触发调度。"),
("asyncio.create_task 和 asyncio.ensure_future 的区别是什么?",
"create_task 是更现代的写法,语义清晰,专门用来把协程包装成 Task。ensure_future 更通用,可以接受协程或 Future两者在 3.7+ 基本等价。"),
("asyncio 里有锁吗,怎么用?",
"有 asyncio.Lock用 async with lock: 来获取,使用时不能阻塞事件循环。计数器、限流等场景可以用 asyncio.Semaphore。"),
("asyncio 的 sleep 和 time.sleep 有什么区别?",
"asyncio.sleep 不会阻塞事件循环会让出控制权允许其他协程运行。time.sleep 会阻塞整个线程,包括事件循环,所以异步代码里绝对不能用 time.sleep。"),
("Python 异步 HTTP 请求用什么库?",
"同步用 requests异步可以用 aiohttp支持客户端和服务端或 httpx3.7+ 支持 async。FastAPI 内部用的就是 httpx。"),
("asyncio 异常怎么处理?",
"协程里直接用 try/except和同步代码一样。多个任务的话 gather(task1, task2, return_exceptions=True) 可以捕获异常而不中断其他任务。"),
("asyncio 如何限制并发数?",
"用 asyncio.Semaphore 控制同时运行的任务数async with semaphore: await coro()。或者用信号量配合 gather 控制总体并发。"),
("asyncio 的取消机制是怎样的?",
"task.cancel() 会抛出 CancelledError被 await 的协程捕获后协程停止执行。Shield 可以保护某个协程不被外部取消。"),
("Python 异步生成器怎么用?",
"async def mygen(): yield x 这样的就是异步生成器,用 async for 遍历。不能直接 list() 转成列表,要用 [item async for item in agen]。"),
("asyncio 里有条件变量吗?",
"有 asyncio.Condition把一个 asyncio.Lock 包装成条件变量,支持 await cond.wait() 等待通知await cond.notify() 通知等待的协程。"),
("asyncio 如何实现超时控制?",
"用 asyncio.wait_for(coro, timeout) 包裹协程,超时抛出 TimeoutError。asyncio.timeout() 是 3.11+ 的新语法,更简洁。"),
("asyncio 的 Future 和 concurrent.futures 的 Future 有什么关系?",
"asyncio.Future 是 asyncio 专属的只能在事件循环里用。concurrent.futures.Future 是线程池的,可以跨线程。两者不能混用,但可以用 asyncio.wrap_future() 转换。"),
("asyncio 服务怎么优雅关闭?",
"用 signal.signal(SIGINT, handler) 捕获信号handler 里调用 loop.stop()。配合 try/finally 做清理,比如关闭数据库连接、取消所有 pending 的 task。"),
("asyncio 的 run_in_executor 什么时候用?",
"当你要调用一个不支持异步的阻塞代码(比如同步的 DB 驱动、文件 I/O用 run_in_executor 把这个调用放到线程池执行,不阻塞事件循环。"),
("Python 异步上下文管理器怎么写?",
"实现 __aenter__ 和 __aexit__ 两个方法,返回值赋给 async with 的 as 部分。async with 会自动调用 __aexit__ 即使有异常抛出。"),
("asyncio 如何处理 CPU 密集型任务?",
"CPU 密集型任务用 asyncio 的事件循环没有意义,因为 GIL 导致无法真正并行。用 loop.run_in_executor 放到进程池,或者直接用 multiprocessing。"),
("asyncio 事件循环可以嵌套吗?",
"nest loop = asyncio.new_event_loop(); nest.run_until_complete(coro()) 这样可以创建嵌套循环,但建议尽量避免,复杂度和调试难度都很高。"),
("asyncio 的 wait_for 和 shield 区别是什么?",
"wait_for 对协程设超时超时取消任务。shield 保护某个协程不被外部取消,但 shield 本身可以设置超时。两者用途不同,可以组合使用。"),
("Python 异步迭代器是什么,和异步生成器有什么区别?",
"异步迭代器需要实现 __aiter__ 和 __anext__返回 awaitable。异步生成器是 async def 里直接 yield更简单直接。多数场景用异步生成器就够了。"),
("asyncio 如何实现心跳/keepalive 机制?",
"用 asyncio.create_task() 创建长循环任务while True: await asyncio.sleep(interval); await send_ping()。配合 try/except 捕获连接断开异常。"),
("asyncio 的 Future 结果怎么获取?",
"在协程里用 result = await future 获取,或者 future.result() 在协程外获取不推荐可能抛异常。callback 可以用 future.add_done_callback(fn)。"),
]
# ---- 话题3PostgreSQL轮次 3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 67, 71, 75, 79, 83, 87, 91, 95, 99----
TOPIC3_PAIRS = [
("PostgreSQL 的 MVCC 机制是怎么工作的?",
"MVCC 通过每行数据的 xmin/xmax 两个隐式字段实现。读操作不会阻塞写,写也不会阻塞读。每个事务看到的是某个快照,数据版本通过 tuple 链组织,垃圾回收由 VACUUM 处理。"),
("PostgreSQL 的 EXPLAIN ANALYZE 怎么用?",
"EXPLAIN ANALYZE 会实际执行查询并显示计划树的每个节点耗时,包括 actual time、rows、loops。EXPLAIN 只看计划不执行。两者结合能定位慢查询的根源。"),
("PostgreSQL 索引有哪些类型?",
"B-tree默认适合等值和范围查询、Hash只支持等值、GiST几何、全文搜索、SP-GiST空间分区、GIN数组、JSONB、BRIN物理顺序范围"),
("PostgreSQL 的 WAL 是什么,有什么用?",
"WAL 是预写日志,每次修改数据先写日志再写数据文件。保证崩溃恢复能力,支持主从复制流复制,也是 CDC变更数据捕获的基础。"),
("PostgreSQL 的 TOAST 机制是什么?",
"TOAST 把超过页面大小8KB的列值压缩或切片存到 toast 表用指针引用。查询时自动组装对应用透明。varlena 类型字段自动支持。"),
("PostgreSQL 的查询优化器怎么工作的?",
"根据统计信息pg_statistic估算每个执行计划的代价选择代价最低的。考虑因素包括行数、索引可用性、join 顺序、排序成本。用 EXPLAIN 查看计划。"),
("PostgreSQL 的 VACUUM 为什么要运行?",
"VACUUM 清理 dead tuples回收空间并更新统计信息防止表膨胀和 MVCC 性能退化。autovacuum 自动运行,但大批量 DELETE/UPDATE 后可能需要手动执行。"),
("PostgreSQL 的分区表怎么做?",
"用 RANGE 或 LIST 分区CREATE TABLE t (...) PARTITION BY RANGE (created_at)。子表自动继承父表结构。查询优化器会自动裁剪不相关分区,性能提升明显。"),
("PostgreSQL 的 JSONB 和 JSON 有什么区别?",
"JSONB 存储时解析并建索引查询更快但插入稍慢。JSON 存原始文本。JSONB 支持GIN索引JSON不行。如果要频繁查询 JSON 内容,用 JSONB。"),
("PostgreSQL 的 COPY 和 INSERT 性能差多少?",
"COPY 是批量导入,比单条 INSERT 快 5-10 倍以上。COPY 适合数据迁移初始导入INSERT 适合单条或小批量。大量数据用 \\COPY 或 COPY FROM STDIN。"),
("PostgreSQL 的连接池用什么方案?",
"应用层连接池用 PgBouncer事务级连接池最常用或 Pgpool-II。也可以用数据库代理如 Supbase 的 pgboat。连接复用能显著降低连接建立开销。"),
("PostgreSQL 的 CTE 和子查询有什么区别?",
"CTEWITH 子句)是命名临时结果集,可读性更好,支持递归查询。简单场景子查询和 CTE 性能差不多,复杂查询 CTE 优化器处理更清晰。"),
("PostgreSQL 的数组类型怎么建索引?",
"数组用 GIN 索引CREATE INDEX ON t USING GIN (arr)。也可以用数组操作符 @>、<@ 查询。GIST 适合含包含关系GIN 适合相等和重叠查询。"),
("PostgreSQL 的触发器有什么应用场景?",
"审计日志(记录变更历史)、自动维护(更新汇总表)、强制约束(跨字段校验)、数据同步到其他系统。创建用 CREATE TRIGGER配合 WHEN 条件过滤。"),
("PostgreSQL 的窗口函数是什么?",
"窗口函数在一组行上计算聚合但不折叠结果集,语法是 func() OVER (PARTITION BY ... ORDER BY ...)。常用ROW_NUMBER()、RANK()、SUM() OVER、LAG/LEAD。"),
("PostgreSQL 的并发控制用 MVCC 和锁有什么区别?",
"MVCC 是乐观并发控制,读不阻塞写、写不阻塞读,通过版本链实现。锁是悲观控制,分 SHARE/EXCLUSIVE 锁模式,用于 DDL 和 SERIALIZABLE 隔离级别。"),
("PostgreSQL 的索引失效有哪些情况?",
"对索引列做函数运算改用表达式索引、LIKE '%%' 前缀通配、类型转换字符串存数字、统计信息不准ANALYZE、隐式类型转换。"),
("PostgreSQL 的 NOTIFY 和 LISTEN 适合什么场景?",
"数据库触发后向应用推送通知,适合:任务队列(放弃 LIST NOTIFY 组合)、实时通知、跨表联动。消息不可靠不持久,需要补强。"),
("PostgreSQL 的行安全策略RLS怎么用",
"ALTER TABLE t ENABLE ROW LEVEL SECURITY。创建策略CREATE POLICY p ON t FOR SELECT USING (user_id = current_user)。为每用户配置后,查询自动过滤。"),
("PostgreSQL 的逻辑复制和物理复制区别是什么?",
"物理复制复制整个数据目录,基于 WAL 粒度,副本是精确一致的。逻辑复制基于复制槽和 WAL 解析,可以复制单个表,支持异构数据库迁移。"),
("PostgreSQL 的 pg_stat_statements 怎么用?",
"pg_stat_statements.track = 'top' 开启后记录查询统计信息包括调用次数、总耗时、I/O 时间。查 pg_stat_statements 找最慢的查询。"),
("PostgreSQL 的物化视图和普通视图有什么区别?",
"普通视图每次查询实时计算,物化视图把结果存成快照,用 REFRESH MATERIALIZED VIEW 更新。适合不要求实时但查询代价高的复杂聚合。"),
("PostgreSQL 的自增主键用 SERIAL 还是 IDENTITY",
"SERIAL 是旧语法,依赖 sequence。IDENTITY 是 SQL 标准语法,更严格,支持 ALTER TABLE。推荐用 IDENTITY GENERATED ALWAYS AS IDENTITY。"),
("PostgreSQL 的 JOIN 类型有哪些?",
"INNER JOIN默认、LEFT/RIGHT/FULL OUTER JOIN、CROSS JOIN笛卡尔积、LATERAL JOIN子查询引用父表列。LATERAL 适合需要子查询引用外层字段的场景。"),
("PostgreSQL 的全文搜索怎么配置?",
"建 tsvector 列和 GIN 索引ALTER TABLE t ADD COLUMN fts tsvector。查询用 to_tsquery/to_tsvector 配合 @@ 操作符,支持中文需要配置分词插件。"),
]
# ---- 话题4Git轮次 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100----
TOPIC4_PAIRS = [
("Git 的 rebase 和 merge 有什么区别?",
"merge 把两个分支合并成一个提交保留完整历史分支结构清晰。rebase 在目标分支上重放当前分支的提交,历史线性但会改写提交 hash公共分支不要 rebase。"),
("Git 的 reset、revert、checkout 区别是什么?",
"reset 移动 HEAD 和分支指针分三种模式soft/mixed/hard会改写历史。revert 创建新提交来撤销旧提交不改历史适合公共分支。checkout 切换分支或恢复文件。"),
("Git 的 stash 命令有什么用途?",
"stash 暂存当前工作目录和暂存区的修改,保存现场后可以切分支做别的事。用 git stash pop 恢复并删除,或 git stash apply 只恢复不删除。"),
("Git 怎么撤销已经 push 的提交?",
"如果是公共分支,用 git revert HEAD 创建新撤销提交再 push。如果是自己分支还没人 pull可以 git reset --hard HEAD~1 再 force push但风险较大。"),
("Git 的 cherry-pick 怎么用?",
"git cherry-pick <commit-hash> 把指定提交在当前分支上重新应用,生成新 hash。适合把 hotfix 或特定功能从一个分支挑到另一个。"),
("Git 的工作流有哪些?",
"常见的有 Git Flow五种分支、GitHub Flow主分支+功能分支、Trunk-Based Development频繁合入主干。选哪个看团队规模和发布周期。"),
("Git 的钩子怎么配置?",
"在 .git/hooks/ 目录下放脚本pre-commit、commit-msg、pre-push 等。shell 脚本或其他语言都可以,示例脚本默认带 .sample 后缀,去掉后缀即可生效。"),
("Git 的 submodule 适合什么场景?",
"当一个仓库需要引用另一个特定版本的仓库时用 submodule。比如第三方库或共享配置仓库。注意 submodule 的 commit 不会自动更新,需要单独拉取。"),
("Git 怎么查看某次提交改了什么?",
"git show <commit> 查看某次提交的完整 diff。git log -p 可以看历史每个提交的变更。git diff commit1 commit2 对比两个提交间的差异。"),
("Git 的 reflog 怎么用来做灾难恢复?",
"reflog 记录所有 HEAD 移动的历史,包括被删除的提交和 reset 操作。git reflog 找到操作前的 hash用 git checkout 或 git reset 恢复到那个状态。"),
("Git 的 alias 怎么配置?",
"git config --global alias.co checkout 用 co 代替 checkout。或者在 ~/.gitconfig 里直接写。还有一种写法 git config --global alias.lg 'log --graph --oneline'"),
("Git 的 bisect 怎么用来定位 bug",
"git bisect start 开始二分查找,标记已知 good 和 bad 提交,自动跳转到中间提交测试。重复直到找到第一个引入 bug 的提交。自动化脚本配合更好用。"),
("Git 的 merge冲突怎么解决最规范",
"先拉取最新代码到本地,合并目标分支,人工解决冲突后 git add 标记已解决git commit 完成合并。不要用 --no-commit 自动合并。"),
("Git 的 fetch 和 pull 区别是什么?",
"fetch 只拉取远程分支更新不合并。pull 是 fetch + merge自动合到当前分支。网络不稳定时 fetch 更安全,可以先看差异再决定是否合并。"),
("Git 的 blame 怎么用?",
"git blame file 逐行显示最后修改的提交和作者,配合 -L 限制行范围。适合追溯某行代码是谁写的、为什么改,但注意不要用于人身攻击。"),
("Git 的 sparse-checkout 怎么配置?",
"git sparse-checkout set <paths> 只检出指定目录或文件减少克隆大仓库的时间和空间占用。git sparse-checkout init --cone 开启更高效的模式。"),
("Git 的 bundle 命令有什么用途?",
"git bundle create file.branch HEAD..feature 把分支打包成一个文件,方便通过网络拷贝或邮件传输。接收方 git bundle pull 导入。适合网络受限时传输分支。"),
("Git 的 clean 命令怎么用?",
"git clean -n 预览要删除的未跟踪文件git clean -f 实际删除。git clean -fd 删除文件和目录。忽略的文件先 git update-index --assume-unchanged 或加 .gitignore。"),
("Git 的 describe 命令有什么输出?",
"git describe 输出版本号格式:最近标签名 + 距离标签的提交数 + g<hash>。用于程序化获取版本号,比 git rev-parse HEAD 更友好。"),
("Git 的 worktree 和 submodule 有什么区别?",
"worktree 从同一仓库检出多个工作目录适合同时在多个分支上工作但不切换。submodule 是嵌套仓库,指向另一个仓库的特定版本。"),
("Git 的 hook 能做什么自动化的事?",
"pre-commit 做代码风格检查和单元测试commit-msg 规范提交信息格式pre-push 禁止不合规范的 pushpost-receive 自动部署。团队统一配置放在仓库里。"),
("Git 的 log 怎么配合 grep 过滤提交?",
"git log --grep='keyword' 搜索提交信息。git log -S 'code' 搜索代码变更历史。git log --author='name' 按作者过滤。组合使用可以精准定位。"),
("Git 的 rev-parse 有什么用?",
"git rev-parse HEAD 输出 HEAD 的 hash--git-dir 输出仓库路径,--show-toplevel 输出项目根目录。常用于脚本里获取仓库相关信息。"),
("Git 的 Interactive Rebase 怎么用?",
"git rebase -i HEAD~n 打开交互式编辑器,列出自最近的 n 个提交。可以 squash合并、reword改信息、drop删除、reorder调换顺序保存后自动执行。"),
]
# ============================================================
# 构建100轮对话
# ============================================================
print("构建100轮对话...")
# 合并所有话题的问答对
all_pairs = []
for i in range(25):
all_pairs.append(("T1", TOPIC1_PAIRS[i]))
all_pairs.append(("T2", TOPIC2_PAIRS[i]))
all_pairs.append(("T3", TOPIC3_PAIRS[i]))
all_pairs.append(("T4", TOPIC4_PAIRS[i]))
for i, (topic, (q, a)) in enumerate(all_pairs, 1):
GATE.add_turn(q, a)
if i % 25 == 0:
print(f" 完成 {i}/100 轮")
print(f"\n总计添加 {GATE.turn_counter} 轮对话")
# ============================================================
# 验证1话题隔离测试
# ============================================================
print("\n" + "="*60)
print("验证1话题隔离——问话题4时前3个话题不应出现")
print("="*60)
# 问一个话题4的代表性Query
q4 = "rebase 和 merge 的区别是什么?用具体场景说明"
selected = GATE.select(q4)
recalled_turns = [item['turn_id'] for item in selected]
topic1_turns = [i for i in recalled_turns if i % 4 == 1] # 话题1 → 轮次 1,5,9...
topic2_turns = [i for i in recalled_turns if i % 4 == 2] # 话题2 → 轮次 2,6,10...
topic3_turns = [i for i in recalled_turns if i % 4 == 3] # 话题3 → 轮次 3,7,11...
topic4_turns = [i for i in recalled_turns if i % 4 == 0] # 话题4 → 轮次 4,8,12...
print(f"\nQuery: {q4}")
print(f"召回轮次: {recalled_turns}")
print(f"话题1 (Redis) 被召回: {topic1_turns}")
print(f"话题2 (Python asyncio) 被召回: {topic2_turns}")
print(f"话题3 (PostgreSQL) 被召回: {topic3_turns}")
print(f"话题4 (Git) 被召回: {topic4_turns}")
污染 = (topic1_turns or topic2_turns or topic3_turns)
隔离结果 = "✅ 无污染" if not 污染 else f"❌ 有污染话题1-3被召回"
print(f"\n隔离验证: {隔离结果}")
# ============================================================
# 验证2召回完整性测试
# ============================================================
print("\n" + "="*60)
print("验证2召回完整性——话题4的关键内容应被完整覆盖")
print("="*60)
# 话题4的锚点关键词
q4_anchors_raw = GATE.anchor_extractor.extract(q4)
q4_anchors = set(a.lower() for a in q4_anchors_raw)
print(f"Query锚点: {q4_anchors}")
# 检查召回的block覆盖了多少锚点
def get_block_anchors(block_dict):
anchors = set()
for t in [block_dict['user'], block_dict['assistant']]:
anchors.update(a.lower() for a in GATE.anchor_extractor.extract(t))
return anchors
covered_anchors = set()
for item in selected:
covered_anchors.update(get_block_anchors(item))
covered = q4_anchors & covered_anchors
missing = q4_anchors - covered_anchors
coverage_pct = len(covered) / max(len(q4_anchors), 1) * 100
print(f"锚点覆盖: {len(covered)}/{len(q4_anchors)} = {coverage_pct:.1f}%")
if missing:
print(f"缺失锚点: {missing}")
完整度 = "✅ 召回完整" if coverage_pct >= 80 else "⚠️ 召回不完整"
print(f"完整度验证: {完整度}")
# ============================================================
# 验证3Token消耗对比
# ============================================================
print("\n" + "="*60)
print("验证3Token消耗对比完整100轮 vs 有门控)")
print("="*60)
def count_tokens(text):
"""粗略估算中文×2英文×1.5符号×0.5"""
import re
chinese = len(re.findall(r'[\u4e00-\u9fff]', text))
english = len(re.findall(r'[a-zA-Z]+', text))
symbols = len(re.findall(r'[^\w\s]', text))
spaces = len(re.findall(r'\s', text))
return int(chinese * 2 + english * 1.5 + symbols * 0.5 + spaces * 0.5)
# 无门控全部100轮
all_text = ""
for b in GATE.blocks:
all_text += b.user_text + b.assistant_text
all_tokens = count_tokens(all_text)
gated_tokens = sum(count_tokens(item['user'] + item['assistant']) for item in selected)
saving = (1 - gated_tokens / all_tokens) * 100
print(f"无门控全部100轮: ~{all_tokens} tokens")
print(f"有门控(仅召回): ~{gated_tokens} tokens")
print(f"Token节省: {saving:.1f}%")
# ============================================================
# 验证4混合话题查询——真实用户行为模拟
# ============================================================
print("\n" + "="*60)
print("验证4混合话题查询——真实用户交替提问行为")
print("="*60)
# 模拟用户在不同话题间跳转,验证每次跳转后的话题隔离
test_queries = [
("第1轮问Redis", "Redis 惰性删除是什么意思?", "T1"),
("第26轮问asyncio", "asyncio.gather 和 asyncio.wait 有什么区别?", "T2"),
("第51轮问PostgreSQL", "EXPLAIN ANALYZE 怎么用?", "T3"),
("第76轮问Git", "git stash 有什么用途?", "T4"),
("第96轮再问Git", "git reset 和 revert 区别是什么?", "T4"),
("第97轮切回Redis", "Redis 主从复制断线后怎么增量同步?", "T1"),
]
print()
for label, query, expected_topic in test_queries:
selected_q = GATE.select(query)
recalled = [item['turn_id'] for item in selected_q]
# 判断召回的轮次属于哪个话题