【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 就没了。
三、根因:配置被绑到会被重建的会话对象
根因拆解:
- 配置进了 session:
session.permissions = load_config(),而每个 prompt 新建 session,新 session 用默认 permissions。 - 只初始化一次后覆盖:启动读一次 P 放到全局,但第一个 prompt 的处理流程里某处
session = new Session()后又session.permissions = DEFAULT,把 P 覆盖。 - 作用域错误:P 是"用户级/应用级"配置,却被当成"请求级"参数在使用链里传递,请求结束即弃。
- 缺少回读:每次请求不从权威配置源(设置文件/配置服务)回读,而是依赖内存里会被冲掉的副本。
下面用最小模型复现第 1、2 类,再给修复。
四、最小可运行复现
class Session: def __init__(self, permissions=None): # 错误:每次新建 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_session=True): 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_session=False)) print("第2个 prompt:", app.handle_prompt("hi", fresh_session=True)) # 第2个变成 {'mode': 'default'} —— 配置丢失运行看到:第一个 prompt 用了 beta 配置,第二个(新建 session 且未注入配置)退化成默认——正是报错的复现。
五、方案:配置与运行时状态严格分离
第一层:配置存在独立、长生命周期的对象里,绝不放进会被重建的 session。session 只持有"指向配置的引用"或运行期派生状态:
class Config: def __init__(self, permissions): self.permissions = permissions # 长期稳定 class Session: def __init__(self, config: Config, messages=None): 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_session=True): 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, messages=None): 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(mode="beta", 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(f"prompt#{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 只持有其引用;
- 注入 + 不可变:配置以只读快照注入处理链,任何环节无法顺手重置;
- 回读兜底:每个请求前校验生效配置与权威源一致,不一致即回读纠正。
核心原则:用户配置是跨请求的稳定事实,必须和每轮都会生灭的会话状态物理分离。只要配置不进 session,session 重建多少次,用户的设定都不会丢。