372 lines
30 KiB
Python
372 lines
30 KiB
Python
"""
|
||
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 <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 禁止不合规范的 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]
|
||
# 判断召回的轮次属于哪个话题
|