Agent 开发踩坑手册(Week 1 – Week 9 实战记录)
全部来自实际作业实测,每条都带「现象 → 根因 → 修法 → 记忆锚点」。
按主题分类,不按时间排序;同一坑在不同周反复出现时合并成一条。
一、Python 语言层:最容易"静默出错"的坑
1.1 原地修改方法返回 None
result = await real_run(msgs.append({...})) # ❌ append 返回 None
现象:参数瞬间变成 None,函数里一操作就 TypeError,或报奇怪的类型错误。
根因:list.append / sort / reverse / extend 都是原地修改,返回 None。
修法:分两行写。
msgs.append({...})
result = await real_run(msgs)
记忆锚点:“原地修改的方法不吃返回值”。同类坑:todos = todos.sort()。
1.2 logging.basicConfig 顺序陷阱
现象:明明设了 level=INFO,INFO 日志一条不输出,或日志文件是空的。
根因:basicConfig 只在 root logger 尚无 handler 时生效。如果你在它之前调过任何 logging.warning() / 第三方库先打了日志,root 已被隐式配置,你的显式配置被静默忽略。
修法:logging.basicConfig(…) 必须放在所有日志调用之前,紧跟 import 之后。
记忆锚点:“配置日志是第一行代码”。
1.3 局部变量未定义 / 缩进错位导致 NameError
现象:print(result) 改成 log.info(result) 后报 NameError,但 print 之前明明正常。
根因:result 定义在 for tc in … 循环体内,调用点在循环外;改缩进时把定义行 和调用行 的层级改错了。
修法:改完缩进后逐行对齐列位置检查;用 python -m py_compile 只能查语法,查不出作用域问题,靠肉眼看层级。
记忆锚点:“NameError 九成是缩进或作用域”。
1.4 f-string 拼接错误产生"孤立字符"
{"content": "你是助理,不要编造"f"今天是{today}。"} # ❌
现象:system 提示词里多出一个莫名其妙的 f 字符;{today} 也没被替换。
根因:f 必须紧贴引号(f"…")。这里 f 前面还有字符串,Python 把它解析成独立字符串字面量 "f" 拼了进去,且后半段是普通字符串,不做插值。
修法:拆成多个字符串,只有需要插值的那段加 f 前缀:
"你是助理,不要编造。" f"今天是{today}。"
或用模板:"你是助理…今天是{today}。".format(today=…)(推荐,便于外置到 prompts.py)。
记忆锚点:“f 只在引号前面才有意义”。
1.5 assert 的消息是惰性求值 & -O 会全局关闭
现象 1:以为 assert x, f"{obj.attr}" 里 f-string 一定执行(有副作用)。
现象 2:线上用 python -O 跑,所有 assert 校验全部失效。
根因:断言通过时消息表达式不求值;-O 模式下 assert 被完全移除。
修法:业务校验用 if not cond: raise ValueError(…),assert 只用于自测/断言不变量。
记忆锚点:“assert 是调试工具,不是业务校验”。
二、配置与工程环境
2.1 requires-python 写低了导致依赖无解
现象:uv add openai-agents 报 “no solution found”,看不出原因。
根因:pyproject.toml 里 requires-python = ">=3.8",等于承诺"3.8 也能装";而 openai-agents 要求 >=3.10。uv 求解时发现宣言与事实矛盾,直接无解。
修法:改成你实际使用/测试的版本下限,如 ">=3.11"。
记忆锚点:“requires-python 是兼容性承诺,宁高勿低”。
2.2 VIRTUAL_ENV 不匹配警告
现象:uv run 时提示 VIRTUAL_ENV=/home/xxx/venv does not match the project environment path '.venv' and will be ignored。
根因:shell 里还激活着一个旧的 venv,而 uv 用项目自己的 .venv。
修法:deactivate 退出旧环境;之后统一 uv run,不手动 activate。
2.3 装包名 ≠ 导入名
pip install agents ❌ —— 包名是 openai-agents,导入名才是 agents。
记忆锚点:“装包看 PyPI 名,import 看代码名,两者经常不同”(同类:pip install pillow → import PIL)。
三、LLM 多轮对话与会话状态
3.1 假多轮:每轮重建 messages
现象:多轮对话"能跑",但模型完全不记得上一轮。
根因:messages 在每轮函数内部重新初始化为 [system],历史被丢弃。
修法:messages 定义在循环外,每轮 append user → 请求 → append assistant。
记忆锚点:“LLM 无状态,记忆 = 每轮全量重发 messages”。
3.2 system 提示词对老会话不生效
现象:提示词升级到五段式后,行为没变——因为恢复的历史里 messages[0] 还是旧 system。
根因:if not messages: 才写入 system,老会话直接跳过了重建。
修法:加载后无条件替换 system:
if messages and messages[0]["role"] == "system":
messages[0]["content"] = prompt
else:
messages = [{"role": "system", "content": prompt}] + messages
记忆锚点:“system 是配置,对话是数据;恢复会话时配置以当前代码为准”。
3.3 历史污染:旧回答变成"示范"
现象:新会话第一次查天气正常触发工具;同一会话中第二次问,模型直接凭历史回答,不调工具。
根因:上下文里已有"模型直接回答"的记录,模型把它当作示范(in-context few-shot 的负面效应)。
修法:① system 硬规则「每次查询必须重新调用工具,禁止引用历史结果」;② 提示词加 few-shot 边界示例;③ /reset 做对照诊断。
记忆锚点:“坏示范也是示范”——上下文里的一切都会影响行为。
3.4 会话持久化:原子写入三件套
现象:断电/强杀进程后 sessions 文件变成半截 JSON,下次启动直接崩。
修法:
with open(tmp, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path) # 原子替换
再配:损坏文件 os.replace 到 .corrupt-<ts>.bak(不删除)、读失败返回空会话(fail-open)。
记忆锚点:“写临时文件 → flush → fsync → 原子替换”,四步缺一不可。
3.5 只捕获 JSONDecodeError 不够
现象:文件内容不是合法 UTF-8 时,UnicodeDecodeError 直接崩。
修法:except (json.JSONDecodeError, UnicodeDecodeError) 或最后加 except Exception 兜底。
记忆锚点:“读外部文件至少要防两种错:编码错 + 格式错”。
四、Token 计量与裁剪
4.1 len/1.5 估算严重失真
实测:中文场景低估约 44%(实际 token 远多于估算),导致裁剪后仍然超预算。
修法:tiktoken.get_encoding("cl100k_base") 精确计数;不可用时降级为 cjk*1.2 + other*0.3 并打 warning。
记忆锚点:“字符数≠token 数,中文尤其差得多”。
4.2 消息形态不是字符串(SDK 版特有)
现象:count_tokens 里报 TypeError: '<=' not supported between instances of 'str' and 'dict'。
根因:to_input_list() 返回的 SDK 消息,content 可能是 None、列表(如 [{"type":"text","text":"…"}])或字符串。列表被当作字符串逐字符遍历,元素是 dict → 字符串比较爆炸。
修法:
def count_tokens(text) –> int:
if not text:
return 0
if not isinstance(text, str):
text = json.dumps(text, ensure_ascii=False)
...
同时补 tool_calls[].function.arguments 的计数(tool_calls 消息的 content 是 None,参数全在 arguments 里,否则低估)。
记忆锚点:“消息不再由你全权控制时,所有处理函数都要防御形态”(同类坑:ChatCompletionMessage 对象没有 .get,必须 model_dump() 转 dict)。
4.3 裁剪破坏消息配对
现象:裁剪后请求 400,报 tool_call_id 找不到对应消息。
根因:从头部粗暴切片,可能切掉 assistant(tool_calls) 但留下 tool,或留下孤儿 assistant。
修法:从后往前累加保留,最后做配对修正:while kept and kept[0]["role"] == "assistant": pop(0)。
已知边界:assistant(tool_calls) + tool 组的成对裁剪尚未完全处理,工具密集会话约 20 轮后可能触发。
五、结构化输出与 Pydantic
5.1 校验结果被丢弃
Weather.model_validate_json(raw)
return raw # ❌ 白校验了
修法:return Weather.model_validate_json(raw),或接收后返回 obj.model_dump()。
记忆锚点:“validate 的返回值才是验过货的东西”。
5.2 Optional[str] = None 导致字段消失
现象:L2 json_schema 模式下,advice 字段时有时无。
根因:有 default 的字段不进 required,模型可以合法省略。
修法:需要模型必填的字段,不给 default(哪怕是 Optional):advice: Optional[str] = Field(description=…)。
记忆锚点:“default 决定必填性,Optional 只决定可空性”。
5.3 city: str = Field(default=None) 自相矛盾
类型声明 str(非空字符串),默认值却是 None——自相矛盾的规格书。
修法:Optional[str] = Field(description=…)(可空且必填)或 Optional[str] = Field(default=None, …)(可空且可选)。
5.4 required 但可空的两种解法
| 手写 Schema(你 Week 3) | 是 | "required": ["advice"],类型允许 null |
| SDK strict 模式(Week 6) | 是 | 所有字段进 required,可空用 anyOf: [{"type":"string"},{"type":"null"}] |
记忆锚点:“required 管’必须出现’,nullable 管’可以是空’,两者正交”。
5.5 加 error 字段后正常查询报缺字段
现象:为让模型能合法拒绝而加 error 字段,结果正常查天气也报 “missing error”。
根因:error 无 default → 进 required → 模型必须提供。
修法:设计成「全字段 required + 允许 null」,或 error 给 default=None(但那样模型会偷懒不填)。
教训:字段设计的每个选择都有行为后果,Schema 就是产品设计。
六、工具调用(Function Calling)
6.1 三处注册缺一不可
TOOLS(执行体字典)+ TOOLS_SCHEMA(给模型的菜单)+ 函数本体。少一处:菜单里有但执行不了,或能执行但模型不知道。
SDK 版:@function_tool 一处搞定(签名 → Schema,docstring → description)。
6.2 tool_calls 位置找错
现象:resp.tool_calls 不存在。
修法:msg = resp.choices[0].message; msg.tool_calls。
6.3 response_format 与 tools 冲突
同一请求里既给 tools 又给 response_format,两个结构约束打架。修法:二选一。
6.4 工具执行块缩进错位
现象:工具结果只回填了最后一个,或多轮里结果串位。
根因:执行块在 for tc 循环外,只拿到最后一个 tc。
修法:整块缩进到 for tc in msg.tool_calls: 内部。
6.5 字典写成 set
result = {"error", f"未知工具{name}"} # ❌ set,不是 dict
修法:冒号 {"error": f"…"}。记忆锚点:“set 用逗号,dict 用冒号”。
6.6 拼写错误被兜底吞掉
现象:loc["latitud"](少一个 e)→ KeyError → 被 except Exception 吞掉 → 所有查询都走 error 分支,但看起来"程序正常"。
修法:修拼写;同时兜底 except 里必须 log 原始异常类型,否则错误静默。
记忆锚点:“测试通过 ≠ 功能正确”——要么看返回值语义,要么看日志。
6.7 命令拼错被模型"碰巧"响应
现象:用户输 /lsit(list 拼错),没被命令分支拦下,流给了模型,模型"碰巧"输出了一份待办清单。
修法:所有 / 开头先走守门分支;无法识别时报"未知命令"。
记忆锚点:“凑巧不等于真实功能”——能出正确结果不代表代码路径正确。
6.8 geocoding 同名地点陷阱
现象:查"保定"返回 22°C(实际河北保定 13°C)。
根因:Open-Meteo 中文 geocoding 返回的唯一"中国"结果是云南西双版纳保定乡,河北保定根本不在结果里。
修法:工具 description 写明"city 用英文名或拼音,首字母大写",靠模型转 Baoding。
记忆锚点:“中文地名 geocoding 不可靠,让 LLM 做拼音转换”——把语言能力交给擅长的组件。
七、提示词工程(Week 4)
7.1 弱模型偷懒 vs 强模型稳定
实测:同一套 Schema 和提示词,弱模型第二次问天气不调工具;换 Kimi 后每次都调用。
结论:“工具调用可靠性 = Schema 设计 × 提示词约束 × 模型能力,三者取短板”。
7.2 提示词与 Schema 口径不一致
system 说"没时间要询问",Schema description 说"未知可不填"——模型随机选一种行为。
修法:system / Schema description / 工具 description 三处口径必须一致。
7.3 提示词错别字会被"传染"
docstring 里 due 写成 deu,经 @function_tool 自动转成参数 description 原样发给模型。
记忆锚点:“docstring 就是代码,也是给模型的接口契约”。
八、异步(Week 6 新增)
8.1 asyncio.run() 反复调用 → Event loop is closed
现象:while True: asyncio.run(real_run(…)),第一轮正常,第二轮崩 RuntimeError: Event loop is closed。
根因:asyncio.run() 每次创建一次性循环用完关闭。全局 AsyncOpenAI 客户端在循环 A 里建了连接池,第二轮进循环 B 却拿到 A 的旧连接。
修法:一个进程一个事件循环——入口只推一次门,循环内全用 await:
async def real_run(messages: list):
return await Runner.run(agent, messages, max_turns=5)
async def chat():
while True:
user_text = input("你> ").strip()
if user_text.lower() == "/exit":
break
result = await real_run(history) # ← 循环内 await,不再 asyncio.run
print(result.final_output)
if __name__ == "__main__":
asyncio.run(chat()) # ← 全进程唯一一次
记忆锚点:“asyncio.run 是门,一个进程推一次;全局异步资源的寿命必须 ≤ 这个循环”。
边界:循环内反复 asyncio.run() 若协程不触碰任何跨调用存活的异步资源(纯计算协程),并不会崩——所以精确说法是"跨循环复用资源"才致命,而不是"写成循环就必然崩"。
8.2 协程没被 await
现象:RuntimeWarning: coroutine 'real_run' was never awaited,程序什么都没做就结束。
根因:调用 async 函数只创建协程对象,不执行。
修法:asyncio.run(real_run(…)) 或在协程内 await real_run(…)。
8.3 第三方端点 model 名被前缀解析
现象:UserError: Unknown prefix: Openai。
根因:SDK 对 model="Pro/moonshotai/Kimi-K2.6" 按 / 前缀路由 Provider,查不到报错。
修法:显式构造 OpenAIChatCompletionsModel(model=MODEL, openai_client=client),model= 传对象而非字符串。
记忆锚点:“第三方 OpenAI 兼容端点 → 一律用 Model 对象绑死客户端”。
8.4 SDK 默认上报 trace 到 OpenAI 云端
现象:自定义端点运行时 SDK 试图连 OpenAI 追踪服务,报错或超时。
修法:set_tracing_disabled(True)。
8.5 护栏触发抛异常而非静默返回
现象:输入"删除待办"直接 traceback。
根因:tripwire_triggered=True 时 Runner 抛 InputGuardrailTripwireTriggered。
修法:
from agents.exceptions import InputGuardrailTripwireTriggered
try:
result = await Runner.run(...)
except InputGuardrailTripwireTriggered as e:
return f"🚫 {e.guardrail.output_info}"
记忆锚点:“护栏不是静默跳过,是抛异常拦截”——调用方必须接住,否则一次越权请求就把整个会话打死。
8.6 装饰器替换名字:双通道必须用两个名字
现象:给 /todos 命令补了裸函数版本后,模型一对话就报:
UserError: Hosted tools are not supported with the ChatCompletions API.
Got tool type: <class 'function'>, tool: function list_todos
根因:@function_tool 的本质是用 FunctionTool 对象替换原名字,于是出现两面性问题:
- 若名字指向 FunctionTool:命令通道 list_todos() 直接崩('FunctionTool' object is not callable)
- 若为了修命令通道把它写回裸函数:Agent(tools=[…]) 里就躺着一个普通 Python 函数,SDK 无法转成 API 的 tools 参数,发请求前直接拒绝
修法:裸逻辑用 _impl 后缀,包装版绑到另一个名字,两条通道各用各的:
def list_todos_impl() –> dict: # ① 命令通道:直接调用
path = Path(f"{agent.name}.json")
...
list_todos_tool = function_tool(list_todos_impl) # ② 模型通道:进 tools 列表
agent = Agent(..., tools=[get_weather, add_todo, list_todos_tool])
# /todos 命令里:
r = list_todos_impl()
记忆锚点:“agent.tools 只收 FunctionTool;裸函数只活在命令通道”——装饰后的名字服务模型,_impl 名字服务代码,永不交叉。
8.7 存档放 finally:异步退出的唯一保险
现象:模型回复到一半按 Ctrl+C,会话没保存,终端糊一整屏 traceback:
asyncio.exceptions.CancelledError ← 第一幕:协程被取消
During handling of the above exception, another exception occurred:
KeyboardInterrupt ← 第二幕:中断重新抛出
根因:Ctrl+C 在异步里不是"在 input 行抛出",而是取消整个协程——中断从任意 await 点注入,你包在 input() 外层的 except KeyboardInterrupt 根本够不着。任何非 /exit 的退出路径都会漏存档。
修法:try/finally 包住整个会话循环,finally 里存档;顶层再接一次 KeyboardInterrupt 收场:
async def multi_chat() –> None:
args_messages = load_sessions("selfsession")
try:
while True:
try:
user_text = input("你> ").strip()
except EOFError: # Ctrl+D
break # ← break,让 finally 统一存档
if user_text.lower() == "/exit":
break
...
finally:
save_sessions(args_messages, "selfsession") # 所有退出路径都存档
print("\\n会话已保存")
if __name__ == "__main__":
try:
asyncio.run(multi_chat())
except KeyboardInterrupt:
print("\\n再见!") # 第二幕在这里安静收场
记忆锚点:“存档逻辑只放 finally,不放任何分支”——你永远不知道用户什么时候按 Ctrl+C。
8.8 消息形态防御:过滤器不能成为数据黑洞
现象:sessions/ 目录里明明有两个会话文件,/list 只显示一个,日志一片安静。
根因:list_sessions() 用 m["role"] == "user" 统计轮数,而 SDK 消息里可能混有无 role、只有 type 的条目(function_call / function_call_output 那族)→ KeyError → 外层 except Exception: continue → 整个文件被静默跳过。
同一个问题在 load_sessions() 里已经用 _valid_msg 修过,list_sessions() 忘了同步——同一坑在两个消费点各踩一次,这就是形态校验没收敛成单一函数的代价。
修法:① 取值一律 .get();② 形态校验收敛成一个函数,所有消费点共用;③ except 必须留痕:
VALID_ROLES = ("system", "user", "assistant", "tool", "developer")
def _valid_msg(m) –> bool:
if not isinstance(m, dict):
return False
if m.get("role") in VALID_ROLES:
return True
return isinstance(m.get("type"), str) # type 风格条目放行
# list_sessions 里
"turns": sum(1 for m in d.get("messages", []) if m.get("role") == "user")
except Exception as e:
log.warning("跳过无法解析的会话 %s:%s", p, type(e).__name__) # ← 留痕
continue
记忆锚点:“静默 continue 是排查成本最高的代码”——吞掉的每个异常都会变成未来的谜题。这个坑已在三个地方踩过(latitud 被吞 / 会话文件被吞 / list_sessions 被吞),够格上升为肌肉记忆。
另见:4.2(content 非字符串)是同族问题——SDK 消息不保证形态,所有处理函数都要防御。
8.9 asdict() 只带字段不带 property(Week 7)
现象:报告序列化时 KeyError: 'score'——Verdict 明明定义了 score 属性,评估跑得好好的。
根因:dataclasses.asdict() 只处理字段(field),@property 是类上的计算方法,不在实例 __dict__ 里,直接被丢掉。dataclass 本身没有类型检查,score 这种"有 property 看起来就有"的错觉最坑。
修法:序列化前显式补上计算值,或消费端现算:
vd = asdict(v)
vd["score"] = v.score # 显式赋值
# 或者报告统计处直接 sum(v.score for v in verdicts) 现算
记忆锚点:“asdict 复印的是字段清单,property 是后印上去的手写页”——计算属性永远不进 asdict 的结果。
8.10 行尾逗号打包 tuple(Week 7)
现象:'tuple' object has no attribute 'get'——明明每一步拿到的都是 dict。
根因:Python 中 x = next(…), 行尾多一个逗号,把赋值变成了单元素元组打包(x = (value,))。前面所有取 dict 的代码都会延迟到很远的下游才炸,且报错位置和写错位置相隔十万八千里。
修法:删掉行尾逗号。
记忆锚点:“行尾逗号 = 元组打包,= x, 不是 = x”——下游炸出 tuple 字样时,先回溯赋值行找逗号。
8.11 特例不进通用层:forbid_keywords 反模式(Week 7)
现象:裁判漏判某类语义缺陷(实体替换:模型把"斯莫斯"换成美国 Smithfield 却以原名播报),于是想在通用检查里加 forbid_keywords 黑名单。
根因:个例写进通用检查 → 黑名单越来越长 → 每个新个例都要改通用层 → 通用层膨胀成特例堆。且硬检查抓形态(工具调用了吗、city 格式对吗),抓不住语义(答的是不是用户问的那个实体)——层次错位。
修法:语义缺陷归 LLM 裁判(裁判标准加"实体替换视为编造");通用层保持只放普适规则。个例等真实项目需求出现再针对性设计(本次决策:推迟到项目期)。
记忆锚点:“通用层只收普适规则,个例进裁判标准或进坟墓”——黑名单是评估层的糖果,吃多了戒不掉。
8.12 打点记录值而非类型(Week 7)
现象:RECORDER 落盘后 due 检查恒失败,打开日志看到 {"content": str, "due": str}——全是类型名。
根因:写打点时把变量名误当成了字符串字面量({"content": "content"}),记下的是类型不是值。而 args 是 due 检查的唯一可靠证据——打点废了,下游检查全废。
修法:{"content": content, "due": due}——值传进去;打点格式一变,下游所有消费点必须同步排查。
记忆锚点:“打点是证据链的起点,起点记了类型,终点只能验证玄学”。
8.13 配置参数被静默忽略:收了不用(Week 8)
现象:四组切分实验(二三级都切 / 只按二级切 / 切碎 / 切粗)指标一模一样,怀疑"参数没影响"。
根因:函数签名收下了 use_h3 参数,但判断条件里写死了 line.startswith("### ") or line.startswith("## ")——参数被接收、被传参、从未被使用。不报错、不警告、照常出结果。
同类变体:练习文件里复制了一份改造过的切分函数,但实际调用的是 brain 里那份——改的那份是死代码。
修法:参数必须有调用点 + 必须有断言。改完跑一次"参数应改变输出"的测试(chunk 数应变化),数量不变就是没接上。
记忆锚点:“配置被忽略比报错危险十倍”——报错会喊,忽略只会让所有实验得出同一个结论。
8.14 函数多分支返回类型不一致(Week 8)
现象:切分后 chunk 数从 46 暴涨到 10887。
根因:split_long 短路径 return body(字符串),长路径 return parts(列表)。外面 enumerate(split_long(…)) 迭代一个字符串 → 逐字符变成 chunk,全文字符数 ≈ chunk 数。
修法:所有 return 同一类型(return [body])。并在 flush 出口加一道形态断言:assert all(len(c["content"]) > 1 for c in chunks)。
记忆锚点:“返回 list 的函数,每个分支都必须返回 list”——和 8.10(行尾逗号打包 tuple)同族:类型在源头悄悄变了,炸在很远的地方。
8.15 累加器初始化在循环内(Week 8)
现象:Top-3 命中率算出 0.0833、MRR 0.0417,与终端逐条输出严重矛盾。
根因:rank_count = 0、mrr_sum = 0.0 写在 for 循环内部,每条查询都清零,循环结束只剩最后一条的贡献。0.0833 = 1/12、0.0417 = 0.5/12,数字与"只剩最后一条"精确吻合。
修法:所有 _count / _sum 与输出语句同层级(都在循环外),循环内只有 +=。
记忆锚点:“统计结果恰好只反映最后一条数据,就是累加器被清零了”——怪数字不是噪音,是 bug 的自白。
8.16 循环变量残留:分支未赋值却被使用(Week 8)
现象:暂时没炸——但 top = hits[0] 只在部分分支赋值,elif expected is None 分支沿用了上一条查询的 top。若第一条查询就走进该分支,直接 NameError。
根因:Python 变量没有块作用域,上一轮的值会残留;h is top 比较恰好还能跑通,掩盖了缺陷。
修法:分支判断前先统一赋值(if hits: top = hits[0]),或初值显式置 None。
记忆锚点:“能在某条路径上不赋值就不该在该路径上被读”——靠残留值跑通是"碰巧",不是"正确"。
8.17 空字符串拼路径变成真值(Week 8)
现象:.env 里模型路径没配好,程序却试图把项目根目录当模型加载。
根因:Path(BASE_DIR) / os.environ.get("…", "") —— Path / "" 返回目录本身,是个非空、truthy 的路径;if LOCAL_EMBED_PATH: 恒真,本地分支被强行进入。
修法:
_local = os.environ.get("LOCAL_EMBED_PATH", "")
LOCAL_EMBED_PATH = str(BASE_DIR / _local) if _local else "" # 先判空,再拼
记忆锚点:“空串 join 出目录,truthy 骗过 if”——环境变量参与路径拼接前,先判空。
九、跨周通用原则(最重要的一页)
附:踩坑时间线(按出现顺序,可对照复习)
| W1 | basicConfig 顺序、.env 泄露、异常静默 |
| W2 | 假多轮、缺 import sys 导致 fail-fast 失效、UnicodeDecodeError |
| W2.5 | token 低估 44%、裁剪破坏配对、write_text 非原子 |
| W3 | 校验结果丢弃、Optional 默认值、error 字段矛盾、tool_calls 位置、set/dict、latitud 拼写、/lsit 误触发、历史污染、geocoding 同名 |
| W4 | 孤立 f 字符、system 对老会话不生效、提示词错别字、日志被注释导致 INFO 静默 |
| W6 | requires-python 过低、装包名≠导入名、asyncio.run 反复调用、协程未 await、model 前缀解析、tracing 未关、护栏异常、content 非字符串、装饰器换名致双通道塌陷、Ctrl+C 存档丢失、role 缺失致会话被吞 |
| W7 | JUDGE_PROMPT JSON 语法错、model=prompt_version 传错、asdict 不带 property、对象混入报告非 JSON serializable、RECORDER 记类型不记值、行尾逗号打包 tuple、拼写笔误致检查永不执行、city 硬检查缺失(中文城市 URL 编码命中同名乡)、实体替换类语义缺陷只能靠裁判 |
| W8 | 本地模型路径相对性(解析随启动目录漂移)、use_h3 参数静默忽略、split_long 返回 str/list 混用致逐字符爆炸、累加器循环内清零、top 残留变量、### 标题 line[3:] 切错、embed_texts 漏 return、空串 join 路径成真值、max_chars 是截断不是切分 |
网硕互联帮助中心






评论前必须登录!
注册