分析对象code/chapter4/llm_client.py·tools.py·ReAct.py·Plan_and_solve.py·Reflection.py运行环境腾讯云 Lighthouse2C2G / 无 GPU / Python 3.14模型kimi-k2.6Moonshottemperature只接受 1组织 RPM3结论先行本章代码能跑通教材的例子但换一个真实模型/网络中环境会同时暴露 7 个独立问题——其中 2 个是崩溃型4 个是静默降级型不报错但结果错了1 个是设计缺陷型工具失败 → 幻觉。它们并非同一原因需分别修。怎么用这一篇这不是猜可能出错的地方而是把本章代码搬到真实云服务器上跑实际发生的问题记一条算一条。每条结构固定现场报错 → 根因已复现→ 因果链 → 为什么严重 → 分档修复方案。文末有排障原则与可直接执行的诊断速查表。本系列其他文章上一章问题篇第三章可能出现的问题不会报错的那类错误5 份教学代码加固清单本章笔记篇第四章 智能体经典范式构建 · 学习笔记ReAct / Plan-and-Solve / Reflection 云端实测下一章问题篇第五章可能出现的问题两类必错设计与生产不可用全系列目录hello-agent入门学习一、问题换模型即崩 ——temperature被写死1.1 现场Error code: 400 - {error: {message: invalid temperature: only 1 is allowed for this model, type: invalid_request_error}}第一次跑Plan_and_solve.py连计划都没生成出来--- 正在生成计划 --- 正在调用 kimi-k2.6 模型... ❌ 调用LLM API时发生错误: Error code: 400 - ... ✅ 计划已生成: ← 空 ❌ 解析计划时出错: list index out of range --- 任务终止 --- 无法生成有效的行动计划。1.2 根因已在本机复现根因一句话llm_client.py第 28 行把默认温度写死为0而kimi-k2.6作为推理模型只接受temperature1。# llm_client.py:28defthink(self,messages,temperature:float0)-str:# ← 写死 0responseself.client.chat.completions.create(modelself.model,messagesmessages,temperaturetemperature,streamTrue)而三个范式的所有调用都不传 temperature全走默认值response_textself.llm_client.think(messagesmessages)# ReAct / Planner / Executor / Reflection 都是这句1.3 逐步因果链① 代码把 temperature 默认值写死为 0 ↓ ② kimi-k2.6 只接受 temperature1服务端直接 400 ↓ ③ think() 的 except 捕获 400打印错误后 return None ↓错误被吞了调用方只看到一个 None ④ ReAct: 错误LLM未能返回有效响应。 → break 看起来像模型没配合 Planner: response_text None or → ↓ ⑤ 空字符串拿去 split(python)[1] → IndexError → 返回 [] ↓ ⑥ 表象无法生成有效的行动计划——让人以为是**提示词**或**模型能力**问题关键点错误信息400被think()吞掉一路衰减成计划为空排查方向被带偏到提示词上。1.4 为什么说这是必然触发的不是偶发。触发条件只有一个模型只接受特定 temperature。而现在主流推理模型kimi-k2.6、o1 系、部分 DeepSeek 推理版普遍锁定采样温度。教材默认的temperature0保证确定性是一张过时的经验牌——它假设了所有服务都允许自由设温。 这正好印证学习笔记 §2.1 那条“凡是靠模型行为约定的地方都必须能配置、能降级。”温度是服务端能力不该由客户端写死。1.5 修复方案修复 1最小改动本轮采用defthink(self,messages,temperature:float1)-str:# 0 → 1修复 2稳健版从配置读留降级路径# .env: LLM_TEMPERATURE1self.temperaturefloat(os.getenv(LLM_TEMPERATURE,1))defthink(self,messages,temperature:floatNone)-str:temperatureself.temperatureiftemperatureisNoneelsetemperature...修复 3能力协商式推荐遇到 400 且报文里出现temperature时自动降级重试把 temperature 改成服务端允许的值exceptBadRequestErrorase:iftemperatureinstr(e):returnself.think(messages,temperature1)# 一次性降级raise更根本的做法把能否自由设温当能力探测项而不是当默认假设。二、问题隐藏缺陷429 限流被吞成空答案2.1 现象跑到第 4 步答案凭空消失Plan_and_solve.py第一遍完整跑完进程EXIT0没有任何报错但最终答案是空的- 正在执行步骤 4/4: 将周一、周二、周三三天卖出的苹果数量相加得出总销量。 正在调用 kimi-k2.6 模型... ❌ 调用LLM API时发生错误: Error code: 429 - {error: {message: Your account ... reached organization max RPM: 3, please try again after 1 seconds}} ✅ 步骤 4 已完成结果: ← 空 --- 任务完成 --- 最终答案: ← 空2.2 根因429 是可重试错误却被当成终局错误看think()的异常处理exceptExceptionase:print(f❌ 调用LLM API时发生错误:{e})returnNone# ← 不区分错误类型一律返回 None429限流是典型的等一会儿就好的瞬时错误但代码直接放弃并返回None。调用方Executor.execute()又用or 把它变成空串response_textself.llm_client.think(messagesmessages)orhistoryf步骤{i}:{step}\n结果:{response_text}\n\n# 空结果被写进 historyfinal_answerresponse_text# 最终答案 空错误的三层嵌套真实错误组织级 RPM3第 4 次请求被限流 ↓ 被 think() 掩盖 呈现的错误步骤 4 结果为空 ↓ 被 Executor 当作正常输出继续 最终效果程序成功退出交出一份空答案2.3 为什么这是严重缺陷零报错零提示EXIT0日志里那行❌混在流式输出里极易被忽略。成功退出与正确回答是两件事。污染状态空结果被写进history后续步骤若有会带着这条垃圾继续推理。不可重入多步任务里任意一步踩到限流整条链就断——而限流在免费/低配额度下是常态本章实测 RPM 只有 3。设计原则“可恢复的瞬时错误”429 限流、超时、连接重置必须重试“不可恢复的错误”401 鉴权失败、400 参数非法才应该立即终止。原代码把这两类混为一谈了。2.4 修复方案修复 1零代码本轮采用OpenAI SDK自带指数退避重试只需打开上限OPENAI_MAX_RETRIES9python3 Plan_and_solve.py实测加了这个环境变量后Plan-and-Solve 第 4 步顺利通过最终答案正确70个苹果。修复 2在客户端显式区分错误类型fromopenaiimportRateLimitError,APITimeoutError,AuthenticationErrorforattemptinrange(5):try:returnself.client.chat.completions.create(...)except(RateLimitError,APITimeoutError)ase:# 瞬时 → 退避重试ifattempt4:raiseLLMServiceError(str(e))frome time.sleep(2**attempt)exceptAuthenticationErrorase:# 永久 → 立即抛raiseLLMServiceError(fKey 无效:{e})frome修复 3调用方不得把错误降级为空答案response_textself.llm_client.think(messagesmessages)ifresponse_textisNone:raiseRuntimeError(LLM 调用失败中止而非继续空转)# 而不是 or 三、问题设计缺陷搜索工具静默失败智能体转头去编3.1 现场ReAct成功给出了一个编造的答案第一轮 ReAct搜索解析恰好失效每次都返回no result--- 第 1 步 --- Action: Search[latest Huawei phone model 2024 2025] 观察: no result --- 第 2 步 --- Action: Search[Huawei Mate 70 latest flagship phone 2024] 观察: no result --- 第 3 步 --- Action: Search[Huawei Mate 70] 观察: no result --- 第 4 步 --- Thought: The previous three search attempts all returned no results. Rather than continuing with further searches that are likely to fail, I will provide the answer based on my training knowledge. ... 最终答案: ... Kirin 9100 chipset ... 5200mAh battery ... 88W wired fast charging ...这些参数是模型回忆出来的连芯片型号都和它上一句自称的 Kirin 9020 互相矛盾而整个流程没有任何地方提示此答案未经任何外部验证。3.2 根因工具失败被伪装成一次正常的 Observation工具返回空/错误时ReActAgent.run照单全收observationtool_function(tool_input)iftool_functionelsef错误未找到名为 {tool_name} 的工具。self.history.append(fObservation:{observation})# 空字符串也照样写进去提示词里没有任何数据不可信时该怎么做的约束Thought: ... Action: ... 就这两条格式要求对信息来源可靠性零约束后果模型凭经验判断再搜也是空于是用参数化知识补位。这在 LLM 看来完全合理——它不知道你在测试不允许编。3.3 为什么这是最危险的一类缺陷它不报错、不崩溃、还给出一个格式完美、看起来好专业的答案。真实产品里这种工具挂了 → 模型开始编的行为比直接报错危险得多用户看不出这是编的日志看不出Observation 是空串不是异常只有在换一个真有数据的搜索源之后两轮对照才暴露差异本轮 ReAct 第二轮就是对照组见学习笔记 §5.4。3.4 修复方案修复 1让失败显式可辨——工具失败时返回带标记的字符串而不是空串defsearch(query:str)-str:...ifnotapi_key:return[TOOL_ERROR] 搜索不可用未配置 SERPAPI_API_KEY。...ifnotout:returnf[TOOL_EMPTY] 未检索到 {query} 的结果。修复 2在提示词里加来源纪律# 重要提示 - 若 Observation 以 [TOOL_ERROR] / [TOOL_EMPTY] 开头说明**外部信息不可得** 此时**严禁凭记忆作答**必须继续换策略或在 Finish 中明确声明未能获取实时信息。修复 3加一层事实依据校验——Finish的答案若引用不出任何一条非空 Observation标记为未验证答案。 学习笔记 §2.1 的判据在这条缺陷上体现得最清楚ReAct 的可信度等于它 Observation 的真实度。工具一哑火范式就退化成裸 LLM 幻觉机。四、问题tools.py的硬导入拖垮整个工具模块4.1 现场Traceback (most recent call last): File run_react.py, line 2, in module from tools import ToolExecutor File tools.py, line 6, in module from serpapi import SerpApiClient ModuleNotFoundError: No module named serpapiToolExecutor是个纯字典封装跟 serpapi 毫无关系却因为同文件顶部一行硬导入而一起挂掉。4.2 根因与修复# tools.py:6 —— 硬导入fromserpapiimportSerpApiClient一个可选的搜索后端被写成了模块级硬依赖。# 修复可选导入try:fromserpapiimportSerpApiClientexceptException:SerpApiClientNone# 让 search() 在缺包时优雅返回错误串这条我们是真的踩到了一开始我想用伪造一个serpapi桩模块来绕过——这是坏味道。正确做法是让依赖可选 降级而不是伪造它。用一个假替身来掩盖依赖缺失会把跑不了变成假装跑得了。五、顺带发现的几个小问题5.1Planner解析强依赖 python 围栏plan_strresponse_text.split(python)[1].split()[0].strip()# 无围栏 → IndexErrorplanast.literal_eval(plan_str)# 非列表字面量 → ValueError实测第一遍 400 报错时response_text为空split(...)[1]直接IndexError: list index out of range。修法是先做容错提取importre mre.search(r(?:python)?\s*(.*?),response_text,re.DOTALL)plan_strm.group(1)ifmelseresponse_text.strip()5.2Reflection的终止条件不是收敛判据if无需改进infeedbackorno need for improvementinfeedback.lower():break实测两轮迭代全都没触发它——kimi-k2.6 作为推理模型总能找出下一步优化空间于是一路跑到max_iterations。终止条件对模型性格的敏感性远高于对任务是否真的完成的敏感性。修法用结构化判据 边际收益阈值# 要求评审员输出 {need_improve: true/false, reason: ...}# 若连续两轮 need_improve 都为 true 但改动 阈值则强制收敛5.3Finish[...]正则缺re.DOTALL老 bug 换壳重演final_answerre.match(rFinish\[(.*)\],action).group(1)# 多行答案 → None → .group(1) 崩这与第三章FirstAgentTest.py第 196 行是同一个坑Finish[...]内容一旦含换行.*跨不过去re.match返回None。而模型越认真、答案越长越容易换行分段。# 修复mre.match(rFinish\[(.*)\],action,re.DOTALL)final_answerm.group(1).strip()ifmelseaction[len(Finish[):].rstrip(]).strip()5.4Executor把模型的装饰性前缀写进 history实测模型在第 3 步输出了结果: 25个苹果多了个前缀这条被原样写进history后续步骤都会看到它。historyf步骤{i}:{step}\n结果:{response_text}\n\n# response_text 未做规整修法写回前strip()并剥掉结果[:]、答案[:]之类前缀。5.5kimi-k2.6的思维链不在delta.content里llm_client只累加chunk.choices[0].delta.content。推理模型的reasoning_content会被完全丢弃。这本身不算 bug我们本来也只想要答案但会带来一个坑若max_tokens设得太小推理就把预算吃光content直接为空。实测{model:kimi-k2.6, ... message:{role:assistant,content:,reasoning_content:The user wants me to reply...}, finish_reason:length} ← max_tokens20 时content 是空的六、由这次排障引出的设计原则三类问题各不相同但折射出构建智能体时的四条通用原则。原则 1瞬时错误必须重试永久错误才该终止429 / 超时 / 连接重置 → 退避重试401 / 400 参数非法 → 立即终止。原代码把两类都变成return None于是限流被静默降级成空答案。错误类型处理方式本轮例子瞬时退避重试SDK 自带 / 手写429 RPM3 →OPENAI_MAX_RETRIES9解决永久抛异常、立即终止400temperature非法 → 必须改配置原则 2“空结果不是正常结果”——失败必须显式最危险的不是异常是返回值语义不明None和和no result在调用方看来差不多于是工具挂了被当成工具说没有。让失败带标记[TOOL_ERROR]让调用方能识别。原则 3依赖要可选、要降级不要伪造from serpapi import ...要么变成try/except可选导入要么延迟到真正调用时再导。在顶部硬导入一个可选后端等于让一个小功能劫持整个模块。原则 4“进程退出码0不等于任务成功”本轮两个成功退出的案例空答案的 Plan-and-Solve、编造答案的 ReAct都是EXIT0。评测智能体必须看答案质量与依据不能只看跑没跑完。七、快速诊断速查表# 1) 模型与密钥是否可用应为 HTTP 200400 多为参数问题401 为鉴权404 为地址python3 -PY import os, json, urllib.request for line in open(.env): if in line and not line.startswith(#): k, v line.strip().split(, 1); os.environ.setdefault(k, v) req urllib.request.Request( os.environ[LLM_BASE_URL].rstrip(/) /models, headers{Authorization: Bearer os.environ[LLM_API_KEY]}) try: print(models:, urllib.request.urlopen(req, timeout20).status) except Exception as e: print(models ERR:, e) PY# 2) 是否被限流RPM/TPM 上限# 看到 429 / rate_limit → 立刻加export OPENAI_MAX_RETRIES9env|grep-i-Eopenai|proxy|seds/.*/set/# 3) 搜索后端是否可达SerpApi 之外给 ReAct 兜一个可用源python3 -PY import urllib.request for u in [https://www.bing.com/search?qtest, https://serpapi.com, https://gitee.com]: try: r urllib.request.urlopen(urllib.request.Request(u, headers{User-Agent: Mozilla/5.0}), timeout15) print(u, r.status) except Exception as e: print(u, ERR, type(e).__name__) PY# 4) 依赖是否齐全本章踩过的三件套python3-cimport openai, dotenv; print(openai, openai.__version__)python3-cimport serpapi21|tail-1# 缺包时给 tools.py 打可选导入补丁# 5) 跑完别只看退出码看最后一行echoEXIT$?;tail-5run.log|grep-E最终答案|最终生成的代码
阅读完成 · 觉得有帮助?