
1. 项目概述为什么“自定义模型封装”是Agent开发绕不开的硬功夫做Agent开发的朋友最近几个月应该都踩过这个坑明明用LangChain搭好了工具链、写好了Prompt模板、连记忆模块都配上了结果一跑真实业务场景就卡壳——不是响应慢得像在等咖啡煮好就是关键推理步骤莫名其妙崩掉再一看日志全是ModelNotAvailableError或者InvalidResponseFormat。我上个月帮一家做智能客服SaaS的团队做技术评审他们用的是LangChain官方文档里推荐的ChatOpenAI类但客户要求必须对接自家训练的GLM-5.3私有模型API地址、鉴权方式、输入输出格式全都不一样。当时他们试了三种方案硬改LangChain源码、写一堆中间转换层、甚至想用Nginx反向代理做协议伪装——最后全被推翻重来。真正解决问题的是一段不到80行的BaseChatModel子类封装代码。这件事让我意识到“自定义模型封装”根本不是什么高级技巧而是Agent工程落地的第一道门槛。它直接决定了你的Agent能不能用、好不好用、稳不稳当。标题里的“Agent实践2”不是按学习顺序排的第二课而是指你在完成基础Agent编排后必须立刻面对的第二个生死关——模型适配。关键词里反复出现的GLM、vscode glm 官方插件、langflow 如何配置自定义模型服务地址背后全是真实业务场景里对模型可控性的迫切需求。你不需要成为大模型训练专家但必须清楚知道如何让LangChain这类框架“认出”你手里的任意一个模型让它像调用ChatOpenAI一样自然、稳定、可调试。这背后涉及的不是API调用那么简单而是对BaseChatModel抽象契约的深度理解、对HTTP通信细节的精准把控、对异步流式响应的耐心打磨以及对Agent执行生命周期的全局把握。如果你还在用requests.post()拼JSON然后手动解析response那你离生产级Agent至少还差一层封装厚度。2. 核心设计思路从“能跑通”到“可运维”的三层封装逻辑2.1 为什么不能直接用requests——协议适配的隐形成本很多刚接触Agent开发的朋友第一反应是“不就是发个HTTP请求吗用requests库几行代码搞定。”我试过也劝退过至少五个团队。表面看确实简单import requests def call_glm_api(prompt): response requests.post( http://your-glm-server/v1/chat/completions, json{model: glm-5.3, messages: [{role: user, content: prompt}]}, headers{Authorization: Bearer your-token} ) return response.json()[choices][0][message][content]但问题很快浮现Agent框架需要的不只是返回文本它需要结构化消息含role、tool_calls、流式响应支持用于前端实时打字效果、token统计用于成本控制、错误分类区分超时、限频、模型内部错误。而requests返回的原始JSON和LangChain期望的AIMessage对象之间隔着整整一个协议层。更麻烦的是不同模型API的字段命名千奇百怪GLM用choicesQwen用outputMinimax用dataDeepSeek的v4版本又把tool_calls塞进function_call字段里。如果每个模型都写一套if-elif-else解析逻辑代码会迅速变成意大利面条。这就是BaseChatModel存在的根本意义——它定义了一个统一的“模型能力契约”只要实现了_generate和_astream方法框架就能自动处理消息组装、历史管理、回调触发、异常归一化。我的经验是宁可花两天吃透BaseChatModel源码也不要花两周写五套requests胶水代码。2.2 封装的三个层次协议层、模型层、Agent层真正的自定义模型封装不是写一个类就完事而是分三层递进构建第一层协议适配层Protocol Adapter这是最底层负责把任意模型API的原始响应翻译成LangChain标准的ChatResult对象。核心工作包括请求体标准化将LangChain的messages列表含system/user/assistant/tool角色转换为目标模型要求的格式如GLM需要{role: user, content: xxx}而某些国产模型要求{from: user, value: xxx}响应解析器提取content、tool_calls、usage字段并处理常见变体如GLM-5.3的finish_reason字段值可能是stop或length而OpenAI是stop/length/tool_calls错误映射把HTTP状态码429限频、503服务不可用和模型返回的error_code如GLM的10001表示鉴权失败统一转为LangChain的LLMError子类方便上层做重试策略。第二层模型能力层Model Capability这一层决定你的封装类“能做什么”。BaseChatModel提供了几个关键参数model_name不是随便填的字符串而是影响get_num_tokens计算逻辑的关键标识。比如GLM-5.3和GLM-5.4的tokenizer不同必须传入正确的model_name才能准确统计tokenstreaming控制是否启用流式响应。注意很多私有模型服务默认关闭流式你需要在构造函数里显式检查/v1/models端点返回的capabilities字段temperature/top_p等参数不是简单透传而是要做范围校验GLM接受0.0~1.0但某些模型只接受整数0~100。第三层Agent集成层Agent Integration这才是最终目标——让封装好的模型无缝融入Agent工作流。重点在于bind_tools方法的兼容性当Agent需要调用工具时LangChain会自动在Prompt末尾注入tools描述并期望模型返回tool_calls。但GLM-5.3的Flash Thinking模式对tool_calls格式有特殊要求必须是JSON数组且id字段为字符串这就需要在_generate里做预处理with_config的上下文传递Agent执行时会通过configurable参数传递session_id、user_id等上下文你的封装类必须能接收并在HTTP Header中透传比如加X-Session-ID头异常熔断当模型连续3次返回finish_reasonlength截断说明提示词过长应主动触发InputOutputError而非静默失败。这三层不是割裂的而是一个有机整体。我在给某金融客户做POC时发现他们的GLM服务在高并发下会随机返回空choices数组。单纯在协议层加空值判断没用因为Agent框架的Runnable链会在_generate抛异常后中断整个流程。最终解决方案是在模型能力层加入retry_on_empty_choicesTrue参数并在Agent集成层配置RetryPolicy(max_attempts3)。这种跨层协同才是封装的价值所在。2.3 为什么选GLM作为实践样本——国产模型的典型适配挑战标题里明确提到GLM不是偶然。在当前国内Agent开发实践中GLM系列尤其是GLM-5.3/5.4已成为事实上的“国产模型基准”。选择它做封装实践是因为它集中体现了国产模型适配的三大典型挑战第一协议碎片化严重。GLM官方API文档里写着标准OpenAI格式但实际部署时不同厂商提供的GLM服务接口差异极大有的用/chat/completions有的用/v1/chat/completions有的甚至用/api/inferenceHeader里的鉴权字段可能是Authorization: Bearer xxx也可能是X-API-Key: xxx还有用X-Glm-Token的。这种碎片化逼着你必须把协议适配层做得足够健壮。第二功能特性不一致。GLM-5.3的Flash Thinking模式对应热词里的glm 5.3 flash thinking budget需要额外传flash_thinking_budget参数而标准OpenAI接口根本没有这个字段。这意味着你的封装类必须支持“扩展参数”且要确保这些参数只在调用GLM时生效不影响其他模型。第三调试信息不友好。当GLM返回{error: {code: 10002, message: invalid request}}时你根本不知道是messages格式错了还是model参数不存在。相比之下OpenAI的错误信息会明确指出The modelglm-5.3does not exist。这就要求你的协议层必须内置详细的日志记录——不仅记录请求URL和Body还要记录响应Headers里的X-Request-ID方便和后端运维对账。我建议新手从GLM入手不是因为它简单而是因为它“够典型”。当你能把GLM的各种变体都封装好再对接Qwen、DeepSeek、Minimax时你会发现大部分逻辑都能复用只是替换几个字段名和校验规则而已。这就像学开车先练手动挡之后开自动挡反而更得心应手。3. 实操细节拆解从零实现一个GLM-5.3封装类3.1 BaseChatModel的契约与必须实现的方法在动手写代码前必须彻底吃透BaseChatModel的契约。这不是一个简单的基类而是一套运行时协议。LangChain 0.1.x版本中BaseChatModel继承自BaseLLM但它的核心契约只有两个抽象方法_generate和_astream。注意是带下划线的私有方法——这意味着你不能直接调用它们而是由框架在invoke/stream时自动触发。很多人第一次封装失败就是因为试图重写invoke方法结果破坏了框架的回调机制。_generate方法签名如下def _generate( self, messages: List[BaseMessage], stop: Optional[List[str]] None, run_manager: Optional[CallbackManagerForLLMRun] None, **kwargs: Any, ) - ChatResult:关键点在于messages参数是LangChain的BaseMessage子类实例如HumanMessage、AIMessage、SystemMessage不是原始字符串。你必须把它们序列化成目标模型能理解的格式run_manager是回调管理器用于触发on_llm_start、on_llm_end等事件。你的实现里必须调用run_manager.on_llm_start(...)和run_manager.on_llm_end(...)否则Agent的监控、日志、追踪功能全部失效**kwargs里包含了所有模型参数temperature、max_tokens等但要注意有些参数可能被框架提前处理过如max_tokens会被转为max_completion_tokens你需要检查kwargs.get(max_completion_tokens)是否存在。_astream方法签名类似但返回AsyncIterator[ChatGenerationChunk]。这里有个重要陷阱很多国产模型服务不支持真正的Server-Sent EventsSSE流式响应而是返回一个包含所有token的JSON数组。这时你不能简单地yield整个数组而要模拟流式行为——逐个yield数组中的元素并在每次yield后await asyncio.sleep(0.01)否则前端会卡住。我在测试某银行私有GLM服务时就遇到过这个问题服务端返回{choices: [{delta: {content: a}, index: 0}, {delta: {content: b}, ...}]}但前端等待第一个chunk超时。解决方案是在_astream里做“伪流式”拆包。3.2 GLM-5.3封装类的完整实现与关键注释下面是我在线上项目中使用的GLMChatModel类已脱敏并添加详细注释。它经过2000次生产调用验证覆盖GLM-5.3所有主流部署变体from typing import Any, Dict, List, Optional, Iterator, AsyncIterator from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage, ToolMessage from langchain_core.outputs import ChatResult, ChatGeneration, ChatGenerationChunk from langchain_core.pydantic_v1 import root_validator import httpx import json import asyncio from urllib.parse import urljoin class GLMChatModel(BaseChatModel): GLM-5.3模型封装类兼容智谱云API及私有化部署变体 # 必须声明的字段用于LangChain序列化 model_name: str glm-5.3 base_url: str https://open.bigmodel.cn/api/paas/v4/ # 默认智谱云地址 api_key: str timeout: float 60.0 streaming: bool False # GLM特有参数非OpenAI标准 flash_thinking_budget: Optional[int] None # Flash Thinking预算单位毫秒 top_k: Optional[int] None # GLM特有参数控制候选词数量 root_validator() def validate_environment(cls, values: Dict) - Dict: 环境校验确保必要字段存在 if not values[api_key]: raise ValueError(api_key must be provided) if not values[base_url].endswith(/): values[base_url] / # 确保URL结尾有斜杠避免路径拼接错误 return values def _generate( self, messages: List[BaseMessage], stop: Optional[List[str]] None, run_manager: Optional[CallbackManagerForLLMRun] None, **kwargs: Any, ) - ChatResult: 同步生成方法实现 # 1. 构建请求体将LangChain消息转为GLM格式 glm_messages self._convert_messages_to_glm(messages) # 2. 构建请求参数 payload { model: self.model_name, messages: glm_messages, stream: False, # 同步调用不启用流式 } # 3. 注入GLM特有参数 if self.flash_thinking_budget is not None: payload[flash_thinking_budget] self.flash_thinking_budget if self.top_k is not None: payload[top_k] self.top_k # 4. 处理通用参数temperature/max_tokens等 for key, value in kwargs.items(): if key in [temperature, max_tokens, top_p, n]: payload[key] value # 5. 发送HTTP请求 try: # 使用httpx而非requests支持异步且更现代 with httpx.Client(timeoutself.timeout) as client: response client.post( urljoin(self.base_url, chat/completions), jsonpayload, headers{ Authorization: fBearer {self.api_key}, Content-Type: application/json, } ) # 6. 错误处理GLM错误码映射 if response.status_code ! 200: error_detail response.json() error_code error_detail.get(error, {}).get(code, 0) error_msg error_detail.get(error, {}).get(message, Unknown error) # 映射GLM错误码到LangChain标准异常 if error_code 10001: # 鉴权失败 raise ValueError(fGLM Auth Error: {error_msg}) elif error_code 10002: # 请求参数错误 raise ValueError(fGLM Request Error: {error_msg}) elif error_code 10003: # 模型不可用 raise ValueError(fGLM Model Unavailable: {error_msg}) else: raise RuntimeError(fGLM API Error {response.status_code}: {error_msg}) # 7. 解析响应 data response.json() choices data.get(choices, []) if not choices: raise ValueError(GLM response has no choices) # 8. 提取内容和工具调用 message_data choices[0].get(message, {}) content message_data.get(content, ) tool_calls message_data.get(tool_calls, []) # 9. 构建LangChain标准消息 ai_message AIMessage( contentcontent, additional_kwargs{tool_calls: tool_calls} if tool_calls else {} ) # 10. 构建ChatGeneration和ChatResult generation ChatGeneration( messageai_message, generation_info{ finish_reason: choices[0].get(finish_reason, stop), usage: data.get(usage, {}), } ) return ChatResult(generations[generation]) except httpx.TimeoutException: raise TimeoutError(GLM request timeout) except httpx.NetworkError: raise ConnectionError(GLM network connection failed) except json.JSONDecodeError: raise ValueError(Invalid JSON response from GLM) def _astream( self, messages: List[BaseMessage], stop: Optional[List[str]] None, run_manager: Optional[CallbackManagerForLLMRun] None, **kwargs: Any, ) - AsyncIterator[ChatGenerationChunk]: 异步流式生成方法实现 # GLM官方API不支持SSE流式但私有化部署可能支持 # 这里采用兼容方案先尝试SSE失败则降级为轮询 async def _stream_generator(): try: async with httpx.AsyncClient(timeoutself.timeout) as client: # 构建流式请求体 glm_messages self._convert_messages_to_glm(messages) payload { model: self.model_name, messages: glm_messages, stream: True, } # 注入特有参数... if self.flash_thinking_budget is not None: payload[flash_thinking_budget] self.flash_thinking_budget # 发送流式请求 async with client.stream( POST, urljoin(self.base_url, chat/completions), jsonpayload, headers{ Authorization: fBearer {self.api_key}, Content-Type: application/json, } ) as response: if response.status_code ! 200: raise RuntimeError(fStream request failed: {response.status_code}) # 解析SSE流 async for line in response.aiter_lines(): if line.strip() or line.startswith(data:): continue try: # GLM SSE格式data: {choices: [{delta: {content: a}, index: 0}]} if line.startswith(data: ): json_str line[6:].strip() if json_str [DONE]: break data json.loads(json_str) choices data.get(choices, []) if choices and choices[0].get(delta): delta choices[0][delta] content delta.get(content, ) if content: yield ChatGenerationChunk( messageAIMessage(contentcontent), generation_info{delta: delta} ) except json.JSONDecodeError: continue except Exception as e: # SSE失败降级为轮询 yield ChatGenerationChunk( messageAIMessage(content), generation_info{error: str(e)} ) return _stream_generator() def _convert_messages_to_glm(self, messages: List[BaseMessage]) - List[Dict[str, str]]: 将LangChain消息格式转换为GLM要求的格式 glm_messages [] for msg in messages: if isinstance(msg, SystemMessage): glm_messages.append({role: system, content: msg.content}) elif isinstance(msg, HumanMessage): glm_messages.append({role: user, content: msg.content}) elif isinstance(msg, AIMessage): glm_messages.append({role: assistant, content: msg.content}) elif isinstance(msg, ToolMessage): # GLM不原生支持tool_message需转换为user角色 glm_messages.append({role: user, content: fTool result: {msg.content}}) return glm_messages property def _llm_type(self) - str: 返回LLM类型标识用于序列化 return glm_chat_model def _get_num_tokens(self, text: str) - int: 估算token数量GLM-5.3使用jieba分词粗略估算 # 生产环境应接入GLM官方tokenizer此处为简化示例 import jieba return len(list(jieba.cut(text)))这段代码的关键价值不在“能跑”而在“可维护”。比如_convert_messages_to_glm方法它处理了ToolMessage的降级转换——因为GLM-5.3原生不支持tool_message角色所以把工具返回结果当作普通用户输入塞回去。这个细节是我在调试一个金融Agent时发现的当Agent调用“查询账户余额”工具后GLM无法理解tool_message导致后续推理混乱。另外_get_num_tokens方法虽然用了jieba粗略估算但预留了接入官方tokenizer的接口符合“先跑通再优化”的工程原则。3.3 VSCode与LangFlow中的实际配置技巧封装好类只是第一步真正落地还要解决IDE和低代码平台的集成问题。热词里反复出现的vscode glm 官方插件、langflow 如何配置自定义模型服务地址指向的是两个高频场景。VSCode配置要点VSCode的AI辅助插件如GitHub Copilot替代品通常通过settings.json配置模型。关键不是填URL而是理解插件的“模型适配器”机制。以某知名国产插件为例它支持custom模型类型配置如下{ aiAssistant.model: custom, aiAssistant.customModel: { type: glm, baseUrl: http://your-glm-server/v1/, apiKey: your-api-key, modelName: glm-5.3-flash } }但很多人填完发现不生效原因在于插件内部会把customModel对象传给一个CustomModelAdapter类而这个类默认只支持OpenAI格式。解决方案是在插件的extension.js里找到CustomModelAdapter修改其callApi方法加入对GLM响应格式的解析分支。更优雅的做法是利用插件提供的modelProvider扩展点注册一个专门的GLMModelProvider——这需要你写一个TypeScript模块核心就是复用上面Python封装类的逻辑只是用fetch代替httpx。LangFlow配置实战LangFlow的“自定义模型”组件Custom LLM要求你提供一个Python类路径比如my_module.GLMChatModel。但直接填会报错因为LangFlow运行在独立进程里需要确保你的GLMChatModel文件在LangFlow的PYTHONPATH中推荐放在langflow/components/llms/目录下在LangFlow的settings.py里添加CUSTOM_LLM_MODULES [my_module]最关键的一步在LangFlow UI的组件配置面板里model_name字段必须填glm-5.3和代码里一致base_url填http://host.docker.internal:8000/v1/注意容器内访问宿主机要用host.docker.internal不是localhost。我遇到过最坑的案例是客户把GLM服务部署在K8s集群里LangFlow跑在Docker Desktopbase_url填http://10.0.2.2:8000/v1/VirtualBox默认网关结果超时。最后发现Docker Desktop的网络模式变了必须用http://host.docker.internal:8000/v1/。这个细节文档里从不提但线上故障率高达70%。4. Agent集成实操让GLM封装类真正“下地干活”4.1 构建一个能调用天气工具的GLM Agent封装类的价值最终体现在Agent工作流里。我们以一个真实需求为例构建一个能回答“北京今天天气怎么样”并自动调用天气API的Agent。这里的关键不是工具本身而是GLM如何理解工具描述、生成tool_calls、并正确处理工具返回结果。首先定义天气工具from langchain_core.tools import tool import requests tool def get_weather(city: str) - str: 获取指定城市的天气预报 try: # 调用真实天气API response requests.get(fhttp://weather-api.com/v1?city{city}) data response.json() return f{city}今日天气{data[condition]}温度{data[temp]}℃湿度{data[humidity]}% except Exception as e: return f获取天气失败{e}然后构建Agentfrom langchain_core.prompts import ChatPromptTemplate from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.runnables import RunnableConfig # 使用我们封装的GLM模型 glm_model GLMChatModel( model_nameglm-5.3, base_urlhttp://your-glm-server/v1/, api_keyyour-key, flash_thinking_budget2000, # 启用Flash Thinking ) # 构建Prompt模板关键GLM对Prompt格式敏感 prompt ChatPromptTemplate.from_messages([ (system, 你是一个专业的天气助手。请用中文回答简洁明了。), (placeholder, {chat_history}), (human, {input}), (placeholder, {agent_scratchpad}), ]) # 创建Agent agent create_tool_calling_agent( llmglm_model, tools[get_weather], promptprompt, ) agent_executor AgentExecutor(agentagent, tools[get_weather], verboseTrue) # 执行 result agent_executor.invoke( {input: 北京今天天气怎么样}, configRunnableConfig( configurable{session_id: abc123} # 传递会话ID ) ) print(result[output])这段代码看似简单但有几个GLM专属的坑Prompt中的{agent_scratchpad}占位符GLM-5.3的Flash Thinking模式对这个字段的位置极其敏感。如果把它放在{input}后面模型可能忽略工具调用指令。必须严格按(placeholder, {agent_scratchpad})的顺序工具描述的长度控制GLM-5.3对tool_description长度有限制约500字符超过会截断。get_weather的docstring必须精简不能写成长篇说明工具调用后的消息处理当GLM返回tool_calls后LangChain会自动调用工具并把结果塞回{agent_scratchpad}。但GLM-5.3有时会把工具结果当成新问题继续推理导致无限循环。解决方案是在GLMChatModel的_generate方法里检测到tool_calls存在时在返回前强制添加stop finish_reason。4.2 并发与稳定性保障Agent如何扛住1000QPS热词里有ai agent 怎么扛并发这不是理论问题而是血泪教训。我服务过一家电商公司他们的Agent用于商品咨询大促期间峰值QPS达1200。最初用单实例GLM封装结果50%请求超时GLM服务端连接池耗尽30%返回乱码HTTP连接复用冲突20%工具调用失败session_id在并发下错乱。解决方案是三层加固第一层连接池与超时控制在GLMChatModel里httpx.Client必须配置连接池# 替换原来的with httpx.Client(...) as client: limits httpx.Limits(max_connections100, max_keepalive_connections20) transport httpx.AsyncHTTPTransport(limitslimits) client httpx.AsyncClient(transporttransport, timeoutself.timeout)同时timeout不能设死值。我们采用动态超时timeout min(60.0, 5.0 * (len(messages) 1))因为消息越长GLM推理时间越久。第二层会话隔离与上下文透传RunnableConfig.configurable里的session_id必须在HTTP请求中透传。修改_generate方法# 在发送请求前 headers { Authorization: fBearer {self.api_key}, X-Session-ID: run_manager.run_id, # 或从configurable中提取 Content-Type: application/json, }这样GLM服务端就能按session_id做请求排队和缓存避免不同用户的上下文混淆。第三层熔断与降级引入tenacity库做熔断from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10), retryretry_if_exception_type((TimeoutError, ConnectionError)) ) def _safe_generate(self, ...): # 原来的_generate逻辑但要注意熔断不能无脑重试。我们在重试前会检查run_manager的retry_state如果已是第三次重试则返回兜底响应“系统繁忙请稍后再试”而不是让用户干等。4.3 安全与审计Agent框架中的模型调用合规性热词里有agent安全这在金融、医疗等强监管行业是红线。GLM封装类必须内置审计能力输入输出日志每条请求/响应必须记录request_id、model_name、input_tokens、output_tokens、elapsed_time。我们用structlog替代logging支持结构化日志PII过滤在_generate方法开头对messages内容做正则扫描发现身份证号、手机号等敏感信息立即脱敏调用配额控制在_generate里检查run_manager的tags如果包含premium则走高配额通道否则走基础通道。最关键的是模型调用链路的可追溯性。LangChain的CallbackManager支持自定义Handler我们写了一个GLMAuditHandlerclass GLMAuditHandler(BaseCallbackHandler): def on_llm_start(self, serialized, prompts, **kwargs): # 记录请求开始时间、prompt摘要 self.start_time time.time() self.prompt_summary prompts[0][:100] ... if len(prompts[0]) 100 else prompts[0] def on_llm_end(self, response, **kwargs): # 记录响应详情、token消耗、耗时 duration time.time() - self.start_time audit_log { request_id: kwargs.get(run_id), model: response.llm_output.get(model_name, unknown), input_tokens: response.llm_output.get(token_usage, {}).get(prompt_tokens, 0), output_tokens: response.llm_output.get(token_usage, {}).get(completion_tokens, 0), duration_ms: int(duration * 1000), status: success if response.generations else failed } # 发送到审计系统 send_to_audit_system(audit_log)这个Handler注册到GLMChatModel的callbacks参数里就能实现全链路审计满足等保三级要求。5. 常见问题排查与独家避坑指南5.1 典型问题速查表问题现象可能原因排查步骤解决方案AttributeError: GLMChatModel object has no attribute _llm_type忘记实现_llm_type属性检查类定义确认有property def _llm_type(self): return glm_chat_model补充_llm_type属性值必须是字符串Agent调用工具后返回空内容GLM未正确解析tool_calls或agent_scratchpad用verboseTrue运行Agent查看agent_scratchpad内容是否被正确注入检查Prompt模板中{agent_scratchpad}位置确保在{input}之后、{chat_history}之前流式响应前端卡住GLM服务不支持SSE但代码强行启用流式抓包查看HTTP响应确认是否返回text/event-stream在_astream里增加SSE可用性探测失败则降级为轮询finish_reasonlength频繁出现提示词过长超出GLM上下文窗口计算messages总token数对比GLM-5.3的32K限制实现trim_messages逻辑在_generate前截断历史消息工具调用参数错误GLM生成的tool_calls参数名与工具定义不匹配查看tool_calls字段对比工具tool装饰器的参数名在GLMChatModel的_generate里对tool_calls做字段名映射如city_name→city5.2 我踩过的五个深坑与解决方案**坑1GLM-5.3的tool_calls格式