""" 100轮4话题对照实验:验证上下文门控器的话题隔离与召回能力 """ import sys sys.path.insert(0, '/root/.openclaw/workspace/context-gatekeeper') from src.gatekeeper import ContextGatekeeper # ============================================================ # 实验设计 # ============================================================ # 4个话题,每话题25轮,交替提问,总计100轮 # # 话题1:Redis 分布式锁 + 缓存策略 # 话题2:Python asyncio 并发编程 # 话题3:PostgreSQL 查询优化 # 话题4:Git 工作流与分支管理 # # 验证维度: # 1. 话题隔离:问话题4时,前3个话题不被召回 # 2. 召回完整:话题4的相关内容被完整覆盖 # ============================================================ GATE = ContextGatekeeper(token_budget=4000) # ---- 话题1:Redis(轮次 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 和 offset,master 只发送差异部分,比全量同步快很多。"), ("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 节点。"), ] # ---- 话题2:Python 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(支持客户端和服务端)或 httpx(3.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)。"), ] # ---- 话题3:PostgreSQL(轮次 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 和子查询有什么区别?", "CTE(WITH 子句)是命名临时结果集,可读性更好,支持递归查询。简单场景子查询和 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 配合 @@ 操作符,支持中文需要配置分词插件。"), ] # ---- 话题4:Git(轮次 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 把指定提交在当前分支上重新应用,生成新 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 查看某次提交的完整 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 只检出指定目录或文件,减少克隆大仓库的时间和空间占用。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。用于程序化获取版本号,比 git rev-parse HEAD 更友好。"), ("Git 的 worktree 和 submodule 有什么区别?", "worktree 从同一仓库检出多个工作目录,适合同时在多个分支上工作但不切换。submodule 是嵌套仓库,指向另一个仓库的特定版本。"), ("Git 的 hook 能做什么自动化的事?", "pre-commit 做代码风格检查和单元测试,commit-msg 规范提交信息格式,pre-push 禁止不合规范的 push,post-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"完整度验证: {完整度}") # ============================================================ # 验证3:Token消耗对比 # ============================================================ print("\n" + "="*60) print("验证3:Token消耗对比(完整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] # 判断召回的轮次属于哪个话题