【Bug已解决】App stops using the configured beta permissions set after the first prompt 解决方案
【Bug已解决】App stops using the configured beta permissions set after the first prompt 解决方案原始报错App stops using the configured beta permissions set after the first prompt 场景用户在设置里配置了一套 beta 权限集比如允许某些命令、禁止另一些。第一个 prompt 正常按这套配置跑从第二个 prompt 起应用不再用这套配置而是回退到了默认权限——用户精心配的限制第一个请求之后就失效了。 关键词配置加载、配置作用域、运行时状态与配置分离、首轮后状态重置、配置注入。一、现象长什么样操作步骤打开设置配置 beta 权限集 P例如允许读文件、禁止网络发第一个 prompt日志显示用的是配置 P符合预期发第二个 prompt日志里权限变成默认集允许一切/或最严默认P 没了后续每个 prompt 都用默认集P 再没出现。这不是配置被删了——重新打开设置P 还在。问题是运行时没有持续使用 P只在第一个请求里用到了之后就丢失。典型表现是首轮正确、之后全错和配置只在第一次被读取并绑定到某个会被重建的对象上高度吻合。二、背景配置configuration和会话状态session state是两件事应用里容易混淆两类数据配置config用户设定的、跨请求稳定的偏好如权限集、模型选择、主题。应长期存在不因单个请求生灭。会话状态session state处理单个请求时的临时状态如当前消息、工具调用中间结果。每轮可能新建。bug 的根因往往是把配置 P存进了会话状态对象而每个 prompt 都会新建/重置会话状态于是 P 跟着被默认配置覆盖。第一个 prompt 用的是启动时那次残留的 P之后会话状态重建P 就没了。三、根因配置被绑到会被重建的会话对象根因拆解配置进了 sessionsession.permissions load_config()而每个 prompt 新建 session新 session 用默认 permissions。只初始化一次后覆盖启动读一次 P 放到全局但第一个 prompt 的处理流程里某处session new Session()后又session.permissions DEFAULT把 P 覆盖。作用域错误P 是用户级/应用级配置却被当成请求级参数在使用链里传递请求结束即弃。缺少回读每次请求不从权威配置源设置文件/配置服务回读而是依赖内存里会被冲掉的副本。下面用最小模型复现第 1、2 类再给修复。四、最小可运行复现class Session: def __init__(self, permissionsNone): # 错误每次新建 session 都用默认权限覆盖 self.permissions permissions if permissions else {mode: default} class App: def __init__(self): self.config_permissions {mode: beta, allow: [read]} def handle_prompt(self, text: str, fresh_sessionTrue): if fresh_session: # 第一个 prompt 之后每次都新建 session且没把 config 注入 s Session() # 用默认权限 else: s Session(self.config_permissions) return s.permissions if __name__ __main__: app App() print(第1个 prompt:, app.handle_prompt(hi, fresh_sessionFalse)) print(第2个 prompt:, app.handle_prompt(hi, fresh_sessionTrue)) # 第2个变成 {mode: default} —— 配置丢失运行看到第一个 prompt 用了 beta 配置第二个新建 session 且未注入配置退化成默认——正是报错的复现。五、方案配置与运行时状态严格分离第一层配置存在独立、长生命周期的对象里绝不放进会被重建的 session。session 只持有指向配置的引用或运行期派生状态class Config: def __init__(self, permissions): self.permissions permissions # 长期稳定 class Session: def __init__(self, config: Config, messagesNone): self.config config # 持有配置引用不拷贝覆盖 self.messages messages or [] class AppV2: def __init__(self): self.config Config({mode: beta, allow: [read]}) def handle_prompt(self, text: str, fresh_sessionTrue): if fresh_session: s Session(self.config) # 始终注入同一个 config else: s Session(self.config) return s.config.permissions if __name__ __main__: app AppV2() print(第1个 prompt:, app.handle_prompt(hi)) print(第2个 prompt:, app.handle_prompt(hi)) print(第3个 prompt:, app.handle_prompt(hi)) # 全部输出 {mode: beta, allow: [read]}配置独立于 session 生命周期无论新建多少次 session用的是同一个config引用配置不会丢。六、方案配置注入且不可变请求链只读取第二层把配置做成只读快照在处理链里以参数形式注入任何环节都不能顺手重置它from typing import NamedTuple class Permissions(NamedTuple): mode: str allow: tuple class SessionV3: def __init__(self, permissions: Permissions, messagesNone): self.permissions permissions # 不可变快照 self.messages messages or [] def decide(self, action: str) - bool: return action in self.permissions.allow or self.permissions.mode admin class AppV3: def __init__(self): # 配置在应用启动时从设置加载一次之后作为不可变值 self.permissions Permissions(modebeta, allow(read, write)) def run(self, prompts): results [] for i, text in enumerate(prompts): # 每个 prompt 新 session但权限快照始终来自 app.permissions s SessionV3(self.permissions) results.append((i, s.decide(read), s.decide(network))) return results if __name__ __main__: app AppV3() for i, can_read, can_net in app.run([p1, p2, p3]): print(fprompt#{i}: 读{can_read} 网络{can_net}) # 每个 prompt 都稳定读True 网络FalseNamedTuple不可变处理链里任何代码都无法s.permissions DEFAULT悄悄覆盖从类型层面消除被重置的可能。七、方案首个请求后校验配置仍在缺失即回读权威源第三层即便架构对了也要有防御——每个请求开始前校验当前生效权限 配置源权限不一致就从权威源设置/配置服务回读class ConfigStore: def __init__(self): self._source {mode: beta, allow: [read]} def load(self): # 从权威源文件/服务读取最新配置 return dict(self._source) class AppV4: def __init__(self): self.store ConfigStore() self.active self.store.load() def handle(self, text: str): # 防御处理前确认 active 与权威源一致 fresh self.store.load() if fresh ! self.active: self.active fresh # 不一致则回读避免用过期的 return self.active if __name__ __main__: app AppV4() print(app.handle(p1)) print(app.handle(p2)) print(app.handle(p3))回读是兜底万一某处逻辑把active改坏了下一个请求会把它纠正回配置源的值。八、验证把每个 prompt 都用配置锁进测试def test_config_used_for_every_prompt(): app AppV3() out app.run([a, b, c]) # 三个 prompt 都读到同一套 beta 权限 assert all(can_read for _, can_read, _ in out) assert all(not can_net for _, _, can_net in out) def test_session_rebuild_keeps_config(): app AppV2() p1 app.handle_prompt(x) p2 app.handle_prompt(y) assert p1 p2 {mode: beta, allow: [read]} if __name__ __main__: test_config_used_for_every_prompt() test_session_rebuild_keeps_config() print(配置跨 prompt 持久测试通过。)九、排查清单首轮后配置丢失按顺序查存放位置配置存在哪是否存进了会被每个请求重建的 session/request 对象重建点每个 prompt 是否新建 session新建时有没有把 config 注入覆盖点处理链里有没有permissions DEFAULT这类顺手重置作用域配置是应用级/用户级还是请求级是否被正确归类为长期配置回读每个请求是否从权威配置源回读还是只信内存副本不可变配置对象是否可被运行期代码意外改写用不可变类型更安全。日志第二个 prompt 起权限日志是否变默认是则确认配置未被延续。十、小结首个 prompt 后配置不再生效是配置被绑到了会被重建的会话状态上新会话用默认配置覆盖了用户设定。修复三层分离配置存于独立长生命周期对象session 只持有其引用注入 不可变配置以只读快照注入处理链任何环节无法顺手重置回读兜底每个请求前校验生效配置与权威源一致不一致即回读纠正。核心原则用户配置是跨请求的稳定事实必须和每轮都会生灭的会话状态物理分离。只要配置不进 sessionsession 重建多少次用户的设定都不会丢。