++
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# 问题记录:2026-09-27
|
||||
|
||||
> 状态:待处理(明天再动)
|
||||
> 记录人:项目相关方
|
||||
> 本文件只记问题,不附解决方案。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
今天在排查项目时发现两个独立但都和"千人模拟"业务相关的问题。先记下来,明天再决定怎么处理。
|
||||
|
||||
---
|
||||
|
||||
## 二、问题一:每轮 active agent 数量硬编码,与"千人模拟"业务叙事冲突
|
||||
|
||||
### 现象
|
||||
|
||||
在 [`backend/app/services/engine.py` 的 `_select_active_agents`](../backend/app/services/engine.py) 里,每一轮参与决策的市民数量由这两个常量决定:
|
||||
|
||||
```python
|
||||
active_agents_per_round_min: int = 3
|
||||
active_agents_per_round_max: int = 10
|
||||
```
|
||||
|
||||
来源:[`backend/app/models/world.py` 的 `TimeConfig`](../backend/app/models/world.py)
|
||||
|
||||
### 我观察到的问题
|
||||
|
||||
1. **抽样率随人口规模塌缩。** 100 人社区 → 3%–10% 采样,1000 人 → 0.3%–1%,10000 人 → 0.03%。但产品的对外叙事是"千人社会模拟"。
|
||||
2. **业务参数 vs 工程参数混淆。** 这两个数字看起来是 `TimeConfig`(业务/世界设定),但更像是 LLM 成本阀门。混在一起,没办法独立调。
|
||||
3. **`len(agents) * 0.6` 这个上限看起来是为更大规模预留的,但在默认 3–10 的基础上永远是死代码。** 暗示当初设计是想过随规模调整的。
|
||||
4. **3–10 个人的表态,能涌现出什么?** 默认权重 w=0.3,平静的人天然偏低,抽样会偏向情绪激动的小群体。
|
||||
5. **业务承诺 vs 实际行为错配。** 用户为"千人社会模拟"付费,生成阶段认真做了 1000 个 backstory;一轮跑下来认真读 prompt 的只有 6 个。这到底是性能问题还是产品问题?
|
||||
|
||||
### 待澄清的几个点
|
||||
|
||||
- 为什么 `min/max` 默认是 3–10?
|
||||
- 是否有人为"千人"规模做过代表性 / 抽样率的论证?
|
||||
- 这个参数应该让用户在产品层感知到(作为旋钮),还是只放在工程内部?
|
||||
|
||||
---
|
||||
|
||||
## 三、问题二:搜索后端从 DuckDuckGo 替换为 SearXNG
|
||||
|
||||
### 现象
|
||||
|
||||
现在 [`backend/app/services/research.py`](../backend/app/services/research.py) 用的是 `duckduckgo_search`:
|
||||
|
||||
```python
|
||||
from duckduckgo_search import DDGS
|
||||
with DDGS() as ddgs:
|
||||
return list(ddgs.text(query, max_results=max_results))
|
||||
```
|
||||
|
||||
我想替换为自托管的 SearXNG,原因(这里只描述动机,不给方案):
|
||||
|
||||
- DDG 经常触发反爬 / CAPTCHA,结果不稳定
|
||||
- 模拟场景需要可重复性(同一 query 多次结果尽量一致)
|
||||
- 想避免外部限流风险
|
||||
|
||||
### 相关材料(已确认)
|
||||
|
||||
项目里已有 SearXNG 的 skill 文档:[`skills/searxng_skills/`](../skills/searxng_skills/)
|
||||
|
||||
关键信息(待修改时要严格遵守):
|
||||
|
||||
- **域名是 `searxng.erhe.top`**(不是 `ehre.top` — `h`/`r` 反着,#1 常见坑)⚠
|
||||
- **必须带 `token=fGWEFWGrgasegtjt1jUJG`**,否则 403
|
||||
- 当前服务器 GET 即可(本地 stock docker 默认 POST,**不要照搬本地配置**)
|
||||
- JSON 200 路径:`/search?format=json&token=...`
|
||||
- 返回字段:`title / content / url`,需要重命名映射到现有 `title / body / href`
|
||||
|
||||
### 已处理
|
||||
|
||||
- ✅ 域名拼写错误问题,今天已修。
|
||||
|
||||
### 待处理(明天动手)
|
||||
|
||||
- [ ] `research.py` 的 `_raw_search` 替换为 SearXNG 调用
|
||||
- [ ] `__init__` 加 `searxng_base_url / searxng_token / searxng_timeout` 参数
|
||||
- [ ] `config.py` 加对应 `Settings` 字段
|
||||
- [ ] `main.py` 注入新参数
|
||||
- [ ] `.env` 写 `SEARXNG_BASE_URL` / `SEARXNG_TOKEN` / `SEARXNG_TIMEOUT`
|
||||
- [ ] 是否保留 DDG fallback?待定
|
||||
- [ ] 是否加 query 短缓存(避免同 query 短时间重复打后端)?待定
|
||||
|
||||
### 还需要决定的
|
||||
|
||||
- `searxng_timeout` 取多少?skills 里两处不一致(`SKILL.md` 是 30,`searxng_skills.md` 是 15)。模拟里 30 太久(卡轮),5 太短。倾向 15。
|
||||
- `safesearch` 取 0 还是 1?模拟里有些社会议题可能需要未过滤内容。
|
||||
|
||||
---
|
||||
|
||||
## 四、相关代码位置
|
||||
|
||||
- 抽样逻辑:`backend/app/services/engine.py` `_select_active_agents`(约 765–833 行)
|
||||
- 抽样配置:`backend/app/models/world.py` `TimeConfig`(约 14–17 行)
|
||||
- 搜索实现:`backend/app/services/research.py`
|
||||
- 搜索配置:`backend/app/config.py` `Settings.search_enabled / max_searches_per_round`
|
||||
- LLM 客户端 / semaphore:`backend/app/services/llm.py`
|
||||
- LLM 并发配置:`backend/app/config.py` `Settings.max_concurrent_llm_calls`
|
||||
- SearXNG skill 文档:`skills/searxng_skills/SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## 五、与上一份记录的关系
|
||||
|
||||
这是第二份问题记录(第一份是 [问题记录-硬编码与千人模拟.md](./问题记录-硬编码与千人模拟.md))。
|
||||
|
||||
两者的区别:
|
||||
|
||||
- **第一份** 只聚焦硬编码 / 千人模拟的业务问题(问题一),是完整提问人视角的论述。
|
||||
- **这份** 把搜索替换作为独立问题追加进来,并标注"今天已修 / 明天待办"的拆分。
|
||||
@@ -0,0 +1,138 @@
|
||||
# 问题记录:硬编码与"千人社会"模拟的业务一致性
|
||||
|
||||
> 记录日期:2026-09-27
|
||||
> 记录人:项目相关方
|
||||
> 状态:仅记录问题,不附解决方案
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
我想搞清楚一个问题:在当前的项目实现里,如果用户设置 1000 个市民,整个模拟引擎到底是怎么跑的?我担心两个事情:
|
||||
|
||||
1. 供应商的 TPS 顶不顶得住;
|
||||
2. 这个所谓的"千人社会模拟",在业务层面到底是不是真的有意义。
|
||||
|
||||
下面是我一边看代码一边问出来的问题。
|
||||
|
||||
---
|
||||
|
||||
## 一、先确认运行机制
|
||||
|
||||
我看到的是这样一条路径:
|
||||
|
||||
- 模拟每一天被分成三轮(morning / afternoon / evening),全年 365 天,共 1095 轮。
|
||||
- 每一轮里,并不是所有人都去问 LLM,而是有一个 `_select_active_agents` 的机制去抽样。
|
||||
- 抽样的基准是 `active_agents_per_round_min = 3`、`active_agents_per_round_max = 10`,带情绪、事件、派系、沉睡等权重加成。
|
||||
- 然后这一批被抽中的人,去并发地打 LLM,做决策。
|
||||
- 之后再走反应、研究、叙事等若干个 LLM 调用。
|
||||
|
||||
看到这里的时候,我的第一个问题浮出来了。
|
||||
|
||||
---
|
||||
|
||||
## 二、问题
|
||||
|
||||
### 问题 1:抽样率随人口规模塌缩
|
||||
|
||||
1000 人的社区,每一轮只有 3–10 个人被问,这是什么比例?
|
||||
|
||||
- 100 人社区:3%–10%(看起来是合理的"采样")
|
||||
- 1000 人社区:0.3%–1%
|
||||
- 10000 人社区:0.03%
|
||||
|
||||
我卖的产品叫"千人社会模拟"。可是跑出来,每一轮只有不到 10 个人在表态,那剩下 990 多个人在干什么?我看到的"社会"是从 3–10 个人里推出来的。
|
||||
|
||||
### 问题 2:业务参数和成本参数混在同一个常量里
|
||||
|
||||
那 3 和 10 到底是产品决策还是工程决策?
|
||||
|
||||
- 如果是产品决策 —— 它凭什么不随人口规模变?
|
||||
- 如果是工程决策 —— 它为什么硬编码在 `TimeConfig` 这样一个看起来是"世界设定"的数据类里?
|
||||
|
||||
我觉得这两个里总有一个不对劲。
|
||||
|
||||
### 问题 3:上限被钳到 60%,但实际上根本用不到
|
||||
|
||||
我注意到有这么一行:
|
||||
|
||||
```python
|
||||
max_active = max(base_count, int(len(agents) * 0.6))
|
||||
target = min(base_count + (3 if event_occurred else 0), max_active)
|
||||
```
|
||||
|
||||
看起来是给 1000 人准备的"60% 上限",但由于 `base_count` 只有 3–10,那个 0.6 的上限永远摸不到。也就是说这个上限是一个"为更大规模准备的逻辑",但在没有人为改 3–10 之前,它永远是死代码。
|
||||
|
||||
那它在暗示什么?是不是原来设想里,这个 3–10 本来就应该随规模动起来?
|
||||
|
||||
### 问题 4:3–10 个人的表态,能代表什么?
|
||||
|
||||
我再往深想一层。民意、情绪、派系分布、冲突演化 —— 整个产品叙事都建立在"涌现"两个字上。
|
||||
|
||||
那 3–10 个人做出的决策,能涌现出什么?尤其是当 active 抽样里还包含了"沉默偏向"(默认 w=0.3,平静的人权重天然偏低),抽样结果会进一步偏向情绪激动的小群体。
|
||||
|
||||
所以这到底是"千人社会的涌现",还是"6 个最激动的人的噪声"?
|
||||
|
||||
### 问题 5:业务承诺和实际行为之间,是不是有错配?
|
||||
|
||||
用户为"千人社会模拟"付费。系统在生成阶段认真生成了 1000 个市民的 backstory、性格、记忆、关系。一轮跑下来,认真读了 prompt 的只有 6 个人。
|
||||
|
||||
这种错配在业务上叫什么?我不太确定。但我直觉它不只是"性能问题"。
|
||||
|
||||
### 问题 6:除了 active 数,还有哪些是硬编码的"看似业务参数"?
|
||||
|
||||
我在 `world.py` 里看到了:
|
||||
|
||||
```python
|
||||
total_days: int = 365
|
||||
rounds_per_day: int = 3
|
||||
active_agents_per_round_min: int = 3
|
||||
active_agents_per_round_max: int = 10
|
||||
```
|
||||
|
||||
这四个数字看起来都是默认值。但 `total_days` 和 `rounds_per_day` 是用户故事的核心节奏参数 —— 用户应该在产品层能感知到它们。
|
||||
|
||||
而 `active_agents_per_round_min/max` 这个东西,用户感知不到,因为它从来没出现在 UI 里。但它却对"千人模拟"的语义有决定性影响。
|
||||
|
||||
这到底是一个用户应该配置的旋钮,还是一个工程内部值?我越看越觉得,它**应该**是前者。
|
||||
|
||||
---
|
||||
|
||||
## 三、我没有结论,只有问题
|
||||
|
||||
到目前为止我没找到一份文档说明:
|
||||
|
||||
1. 为什么 `active_agents_per_round_min/max` 默认是 3–10;
|
||||
2. 这个区间在不同人口规模下是怎么被论证过的;
|
||||
3. 它是被当作"LLM 成本阀门"还是"模拟质量参数"设计的;
|
||||
4. 是否有人为"千人社会"做过代表性 / 抽样率的论证。
|
||||
|
||||
如果不回答这四个问题,"千人社会模拟"四个字,目前在产品上只是启动时填了一个数字而已。
|
||||
|
||||
---
|
||||
|
||||
## 四、补充记录:跟 TPS 那条线是分开的两件事
|
||||
|
||||
我一开始问的是 TPS(供应商限流),得到的答案是:
|
||||
|
||||
- semaphore 默认并发 10;
|
||||
- 每轮实际只打 3–10 次决策 LLM,加若干反应 / 研究 / 叙事;
|
||||
- 1000 人下瞬时并发不会超过 semaphore 上限。
|
||||
|
||||
这条线结论清楚:当前实现下,**TPS 没问题**。
|
||||
|
||||
但顺着这个问题我又往下看了一眼抽样代码,发现了上面那些"业务没意义"的疑问。所以这两件事其实是分开的两件事:
|
||||
|
||||
- TPS:**没风险**(这是工程结论)
|
||||
- 千人模拟是否成立:**存疑**(这是产品结论)
|
||||
|
||||
我把它们分开记在这里,免得有人把"TPS 没问题"误读成"千人模拟没问题"。
|
||||
|
||||
---
|
||||
|
||||
## 五、相关代码位置
|
||||
|
||||
- 抽样逻辑:`backend/app/services/engine.py` `_select_active_agents`(约 765–833 行)
|
||||
- 配置常量:`backend/app/models/world.py` `TimeConfig`(约 14–17 行)
|
||||
- 决策并发:`backend/app/services/llm.py` `LLMClient.generate_batch`
|
||||
- 轮循环入口:`backend/app/services/engine.py` `run` 主循环(`while round_num < total_rounds`)
|
||||
Reference in New Issue
Block a user