资讯动态

LangChain+Ollama:输出美化与Function-calling

发布时间:2026/10/26 5:41:46 来源:尧图企业网站定制
本地大模型第一次跑通的时候人通常是兴奋的但兴奋劲儿过去得也快。命令行里吐出来一坨没有换行、没有层级的纯文本模型到底回了什么得拿眼睛一行行抠你想让它查个数据、算个式子、读个本地文件它不干反而一本正经地编一个答案给你。这两件事——输出难看、能力受限——基本是所有人在 LangChain 加 Ollama 这条路上从“能跑”走向“能用”时最先撞上的两道墙。这篇就围绕 Ollama 美化输出与 Function-calling 这两个点展开讲清楚终端里的回答怎么变得像人写的、模型怎么从“只会说”变成“能动手”以及中间那些文档里不会写、只有真跑过才踩得到的坑。适合已经把本地模型拉起来、能跑通一次 invoke 的读者也适合还在犹豫要不要上工具调用的人对照着看。1. 先把问题拆开这条链路上到底缺什么1.1 裸输出的三个层次问题很多人以为“输出难看”只是排版问题其实分层看有三层每层对应完全不同的解法。最表层是字符层模型是一个 token 一个 token 往外蹦的你不用流式就只能干等十几秒甚至几十秒屏幕上一动不动用户以为程序卡死了。中间层是格式层模型的回答里带 Markdown 语法###、**加粗**、- 列表、表格竖线全都原样打印终端不认这些标记读起来比纯文本还乱。最底层是结构层当你需要程序接着往下处理这段回答时人看的排版毫无意义你要的是一个能直接json.loads()的对象或者一个能塞进if判断的字段。这三层问题的解法完全不同字符层靠stream接口配合逐块打印格式层靠终端渲染库把 Markdown 转成带颜色和缩进的字符结构层靠输出解析器配合模型的格式遵循能力。把这三件事混在一起谈就会出现“我加了渲染库但模型还是乱说”这种错位——渲染库只管画管不了模型说什么。提醒格式层和结构层是互斥的两种目标。你要给人看就渲染 Markdown你要给程序用就约束 JSON别指望一次输出既漂亮又能被解析那只会两头不讨好。我一般的做法是明确分成两条链路对话链路走流式加渲染数据链路走结构化解析两条链路用不同的提示词、不同的模型参数甚至可以让结构化那条用温度 0、对话那条用温度 0.7。1.2 为什么选 Ollama 本地跑而不是调云端 API这个问题每次聊都会有人问。本地跑的理由其实很朴素一是调试成本低你改一句提示词就要重跑一次走云端一天下来额度和钱都烧得心疼本地的随便跑二是数据不出机器做内部工具或者处理私密文本时这是硬需求三是延迟可控局域网里没有网络抖动的干扰工具调用的多轮往返不会因为某一次请求超时而整个崩掉。代价也很明确模型能力天花板低一截尤其是工具调用的稳定性7B 级别和云端大模型差得不是一点半点。所以我的判断标准是——原型阶段、内部工具、对准确率容忍度高的场景本地跑完全够用要上生产、要处理复杂多步推理本地模型只适合做预处理和分流。这一点想清楚了后面所有的参数调优和兜底设计才有意义。1.3 整体数据流一次带工具的对话经历了什么把链路摊开看会清楚很多。用户在终端敲一句话LangChain 把它包成HumanMessage连同系统提示、历史消息一起发给 Ollama 的 HTTP 接口。如果这次调用绑定了工具请求体里会多一个tools字段里面是一份 JSON Schema 描述告诉模型“有这么几个函数、每个函数要什么参数”。模型读完这些决定是自己直接回答还是吐出一个“我想调用某个函数”的结构化请求。关键在于——模型到此为止了它不执行任何东西。它只是输出了一个意图AIMessage里的tool_calls字段带着函数名和参数。真正干活的是你的 Python 代码拿到函数名去工具表里查把参数喂进去执行把结果包成ToolMessage追加回消息列表再发起第二次模型调用。这一次模型看到工具的真实返回值才会组织成人话回复。整个流程至少两次模型往返如果工具链有依赖关系往返次数还会往上加。理解了这一点你就明白为什么工具调用的延迟是普通对话的两三倍也就明白为什么要把最大轮次卡死。2. 环境和模型准备版本、量化与参数2.1 依赖装法与版本咬合LangChain 这两年拆包拆得比较碎装错包是最常见的第一个坑。核心要装的是三个langchain-core提供消息、工具、解析器这些基础抽象langchain-ollama是官方维护的 Ollama 集成包负责把 HTTP 调用包成 LangChain 的接口langchain本体主要提供 Agent 编排那套东西如果你只做基础调用其实可以不装。以前很多人习惯用langchain-community里的Ollama类那个已经处于维护状态新代码建议直接用ChatOllama。pip install -U langchain-core langchain-ollama rich pydantic版本上我踩过一次印象很深的坑langchain-core升级到 0.3 之后bind_tools的返回类型和tool_calls的字段结构都有微调如果langchain-ollama还是旧版会出现工具调用被识别成普通文本的情况——模型明明吐了正确的 JSON但ai_msg.tool_calls就是空列表。所以这两个包要一起升别单独动其中一个。装完之后跑一句pip show langchain-core langchain-ollama对一下版本确认 ollama 包的依赖声明里要求的 core 版本和你实际装的一致这一步花三十秒能省掉两小时的迷惑。另外rich是终端渲染用的属于可选依赖但装了之后体验差距很大后面会详细说。pydantic现在基本是必装LangChain 内部大量依赖它做参数校验你写结构化输出也绕不开。2.2 模型怎么挑参数量、量化等级与工具能力模型选型直接决定了这篇的两个主题能不能顺利做下去。先说结论工具调用必须要 7B 起步8B 到 14B 是甜点区1.5B 到 3B 的小模型在这件事上基本不可用。不是完全不能用而是它会频繁出现“该调不调”“调错函数”“参数编造”三种毛病调起来的痛苦远超省下来的那点显存。量化等级的选择是个权衡。Q4_K_M 是通用推荐7B 模型落在 4.5GB 左右质量损失在大多数任务上感知不到Q5 和 Q6 质量更好但显存占用上去得快Q8 基本接近原始精度但显存翻倍除非你有 24G 以上的卡否则没必要。我实测下来 7B 模型在 Q4_K_M 下做工具调用函数名识别准确率已经够用真正的瓶颈在参数抽取比如把一个中文城市名正确填进city字段——这块 Q5 会比 Q4 稳一些如果你的工具有强类型参数值得往上提一档。模型规模量化显存占用约工具调用表现适用场景1.5B-3BQ4_K_M1.5-2.5GB经常漏调、参数错纯文本补全、分类7B-8BQ4_K_M4.5-5.5GB单工具稳定多工具偶错大多数本地工具原型7B-8BQ5_K_M5.5-6.5GB参数抽取更稳强类型参数场景14BQ4_K_M8.5-10GB多工具路由基本可用多步工具链32BQ4_K_M19-21GB接近云端小模型复杂编排模型族的选择上目前对工具调用支持比较完整的是各家的 instruct 版本基础版base没有经过对话和工具训练的千万别用。拉模型的时候记得确认 tag 里带的是 instruct 或 chat 后缀或者干脆用默认对话版本。ollama pull qwen2.5:7b ollama list拉完之后先做个基线测试别急着写代码ollama run qwen2.5:7b 你好看能不能正常回再试试中文确认模型本身没问题再往下走。2.3 关键参数num_ctx、num_predict、temperature 的取值逻辑Ollama 有几个参数默认值非常坑不显式设置会直接导致你怀疑人生。num_ctx是上下文窗口长度Ollama 的默认值长期是 2048。这意味着你把一份三千字的文档丢进去模型可能只看到了前两千 token后面的全被截断然后它基于残存的信息一本正经地胡说。这个问题在工具调用场景下更隐蔽——工具返回的结果往往很长加上历史消息很容易撑爆两千的窗口表现就是“模型好像忘了刚才调过什么”。我的做法是统一设成 8192 起步长文档场景直接上 16384 或者 32768。num_ctx和显存的关系要算一下。KV Cache 的占用大致是KV Cache ≈ 2 × n_layers × n_kv_heads × head_dim × seq_len × 精度字节数以 7B 模型、32 层、GQA 下 8 个 KV 头、head_dim 128、fp162 字节为例序列长度 8192 时2 × 32 × 8 × 128 × 8192 × 2 ≈ 1.07 GB序列长度翻到 32768 就是 4.3GB 左右。这个数值加上模型权重4.5GB才是实际显存占用。所以 8GB 显存的卡跑 7B 模型num_ctx设到 16384 就已经很紧张了再往上容易 OOM 或者被系统换页拖到龟速。这个账最好在动手前算一遍别等跑了十分钟发现卡死才回头查。num_predict是单次生成的最大 token 数默认是 -1不限制。不限制的后果是模型偶尔陷入重复循环一直吐同一句话直到把上下文占满才停。设个 2048 或者 4096 作为硬上限既能装下正常回答又能兜住失控的情况。temperature在两条链路上取值完全不同。对话和创意类任务 0.6 到 0.8 比较自然工具调用和结构化输出必须压到 0 到 0.2因为这时候你要的是确定性模型在高温下更倾向于“自由发挥”参数填错的概率明显上升。我一般会给工具调用单独建一个低温度的实例跟对话实例分开管。from langchain_ollama import ChatOllama chat_llm ChatOllama( modelqwen2.5:7b, temperature0.7, num_ctx8192, num_predict2048, ) strict_llm ChatOllama( modelqwen2.5:7b, temperature0.0, num_ctx16384, num_predict1024, )还有一个keep_alive参数值得一提默认模型在闲置 5 分钟后会从显存卸载下次调用又要重新加载那几秒到十几秒的等待很影响体验。开发阶段可以设成keep_alive30m让模型常驻做完再改回去。3. 美化输出从字符流到可读文本3.1 流式输出的正确打开方式流式是所有美化工作的前提。不流式的话用户在终端前面等二十秒看不到任何反馈体验是灾难性的而且大模型的输出本身是逐个 token 生成的invoke只是帮你把这些 token 攒完再返回并没有节省任何时间。LangChain 里ChatOllama提供了stream和astream两个方法返回的是AIMessageChunk的迭代器。最朴素的写法就是遍历打印for chunk in chat_llm.stream(用三句话解释什么是向量数据库): print(chunk.content, end, flushTrue)这里flushTrue是必须的Python 的标准输出有缓冲不加这个参数你会看到内容一段一段地蹦出来而不是平滑地流。end也是必须的否则每个 chunk 后面都跟着换行输出会变成竖排。但这种方式有个隐藏问题chunk.content在某些模型上不一定是纯字符串可能是内容块列表特别是带多模态或者推理标签的模型直接print会打出一堆字典。稳妥的写法是先判断类型for chunk in chat_llm.stream(...): text chunk.content if isinstance(chunk.content, str) else if text: print(text, end, flushTrue)另外流式场景下要知道什么时候结束。迭代器自然结束就是生成完毕但如果模型因为num_predict上限被截断你是感知不到的用户会看到一个说到一半的句子。可以在循环结束后检查一下累计输出的长度接近上限时给个提示或者干脆把上限调大。3.2 终端里的 Markdown 渲染拿到流式文本之后直接打印就是原样的 Markdown 源码**和###全都露在外面。这时候上rich。它的Markdown类能把文本渲染成带 ANSI 颜色、带缩进、带项目符号的终端输出代码块还会自动加背景色和语法高亮观感提升非常明显。难点在于流式和渲染的冲突rich的Markdown是一次性渲染整段文本的你不能每来一个 token 就重新渲染一遍那样既慢又会闪屏。合理的做法是用Live上下文配合一个刷新节流——累积字符到一个阈值比如每 80 个字符或者每 0.1 秒才重新渲染一次。import time from rich.console import Console from rich.markdown import Markdown from rich.live import Live console Console() def render_stream(llm, prompt, min_interval0.08, min_chars60): buffer last_render 0.0 last_len 0 with Live(consoleconsole, refresh_per_second12) as live: for chunk in llm.stream(prompt): if not isinstance(chunk.content, str): continue buffer chunk.content now time.time() if now - last_render min_interval and len(buffer) - last_len min_chars: live.update(Markdown(buffer)) last_render now last_len len(buffer) live.update(Markdown(buffer))这段代码的核心是两个节流条件时间间隔和字符增量。只按时间节流模型输出慢的时候会频繁渲染短文本只按字符节流模型输出快的时候会积压。两个条件都满足才渲染实测下来在 7B 模型上很稳。注意事项Live内部的渲染会清屏重绘如果你的程序在流式过程中还要打印日志会把这部分内容冲掉。日志要么走console.print的同一个 console 对象要么干脆在流式结束后再打。还有一种情况要注意模型输出到一半Markdown 语法是不完整的比如只吐了**加粗还没吐结尾的**这时候rich会把它当成普通文本渲染等后续字符到了再重新解析才恢复。这是正常现象不是 bug用户看到的效果是加粗样式延迟半秒生效可以接受。3.3 结构化输出让模型直接吐对象渲染是给人看的解析是给程序用的。LangChain 里结构化输出有三条路难度和稳定性递增。第一条是JsonOutputParser最轻量本质上是把“请输出 JSON”这句话拼进提示词然后把模型返回的文本拿去json.loads。它支持流式部分解析partialTrue时可以在 JSON 还没闭合的时候逐步吐出字段适合做那种边生成边填表单的交互。缺点是模型经常不听话前面加一句“好的以下是结果”后面加一句“希望对你有帮助”JSON 前后一堆废话解析直接失败。第二条是PydanticOutputParser用 Pydantic 模型定义 schema解析器会自动生成format_instructions塞进提示词并在解析时做类型校验和字段补全。这条路的好处是强类型模型少给字段或者给错类型会直接在解析阶段暴露而不是等到业务逻辑里才炸。from pydantic import BaseModel, Field from langchain_core.output_parsers import PydanticOutputParser from langchain_core.prompts import ChatPromptTemplate class BookInfo(BaseModel): title: str Field(description书名不含副标题) author: str Field(description作者姓名) year: int Field(description首次出版年份四位数字) tags: list[str] Field(description不超过三个分类标签) parser PydanticOutputParser(pydantic_objectBookInfo) prompt ChatPromptTemplate.from_messages([ (system, 你是一个信息抽取助手。只输出 JSON不要任何解释。\n{format_instructions}), (human, {text}), ]).partial(format_instructionsparser.get_format_instructions()) chain prompt | strict_llm | parser result chain.invoke({text: ...}) print(result.title, result.year)注意这里用的是strict_llm温度 0。用对话实例跑这个成功率会掉一截。第三条是模型原生的结构化输出也就是利用 Ollama 的format参数传 JSON Schema。这条路最可靠因为约束发生在解码阶段而不是提示词阶段——模型被强制只能生成符合 schema 的 token 序列理论上不可能输出非法 JSON。ChatOllama支持传format参数structured_llm chat_llm.with_structured_output(BookInfo, methodjson_schema) result structured_llm.invoke(...)不过要注意json_schema这条路对模型和 Ollama 版本都有要求老版本只支持formatjson只保证是合法 JSON不保证字段匹配。如果跑起来报错说不支持退回到PydanticOutputParser那条路别在这上面死磕。3.4 提示词层面的格式约束工具再好也架不住提示词写得松。关于格式约束我总结了几个实测有效的做法。系统提示里把“只输出什么”和“不要输出什么”都写明而不是只说前者。只写“请输出 JSON”模型有相当概率给你包一层代码围栏写上“不要使用 json 代码块包裹不要添加任何前后说明文字”遵守率会明显提升。把 schema 放在消息的靠后位置。长上下文里模型对末尾内容的注意力更强format_instructions如果塞在最前面被忽略的概率更高。用ChatPromptTemplate的时候把它放在 human 消息之前、系统消息之后效果比全塞系统消息里好。给一两个示例。小模型对指令的泛化能力弱但对示例的模仿能力强。一个正例一个反例比写三段规则管用。温度压到 0.1 以下。这一条前面提过但在结构化输出上影响特别直接值得再说一遍。最后无论提示词写得多好解析层一定要有兜底。JsonOutputParser外面套一个 try/except失败时用正则从文本里抠第一个{到最后一个}之间的内容再试一次还失败就走重试或者降级返回。这个兜底看起来土但在真实场景里能救回大概百分之十几的失败率。4. Function-calling把“会说”变成“会做”4.1 底层机制模型只负责表态不负责执行这个概念必须先讲透不然后面所有的代码都是照抄。工具调用的本质是一次协议约定你在请求里附上一份工具清单名字、描述、参数的 JSON Schema模型在生成时如果判断需要外部信息就不输出普通文本而是输出一段特殊结构的 token内容大概是“我要调用 query_weather参数是 city示例市”。LangChain 的ChatOllama负责把这段特殊结构解析成AIMessage.tool_calls列表。到此为止模型的工作结束了。它没有执行任何代码没有联网没有读文件它只是表达了意图。后面所有的事都是你的程序在做查工具、执行、把结果塞回去、再问一次模型。这个“再问一次”很关键很多人第一次写的时候忘了第二次调用直接把工具返回值当最终答案打印出来得到一坨 JSON。理解这一点还有个好处你会明白为什么模型会“编造工具调用”——它没有工具的真实返回只能根据训练数据里的模式猜一个合理的调用。温度高了、描述模糊了、上下文里有过类似的调用都可能触发这种幻觉。4.2 工具定义的写法与描述技巧tool装饰器是最省事的写法函数名、类型注解、docstring 三样东西会被自动转成 JSON Schema。from langchain_core.tools import tool tool def query_weather(city: str, unit: str celsius) - str: 查询指定城市的当前天气。 Args: city: 城市名称中文全称例如“示例市”。 unit: 温度单位只能是 celsius 或 fahrenheit。 # 这里换成真实的查询逻辑 return f{city} 当前晴22 度大部分教程到这里就结束了但真正的功夫在 docstring 上。docstring 是模型唯一能看到的函数说明它决定了模型在什么情况下会想起调用这个函数。写得含糊模型就不调写得啰嗦会挤占上下文参数描述缺失模型就会瞎填。我踩过的几个具体问题一是函数名用缩写qw这种模型完全不知道是干嘛的改成query_weather立刻就正常了二是 docstring 只写“查询天气”没写适用场景模型在问“明天要带伞吗”这种间接问题时不会想到调用它补上“当用户询问天气状况、是否需要带伞、穿衣建议时使用”之后就对了三是参数没写枚举范围模型给unit填了个摄氏度加一句“只能是 celsius 或 fahrenheit”能解决大部分。还有个坑是参数类型。str、int、float、bool这些基础类型没问题list[str]也还行但一旦上到list[dict]或者嵌套的 Pydantic 模型小模型基本就懵了要么字段名写错要么结构整个歪掉。我的做法是把复杂参数拆成多个简单参数让模型填平铺的键值对进了函数内部再自己组装。工具数量也要控制。实测下来 5 个以内模型路由得比较准超过 8 个就开始出现“明明该调 A 却调了 B”的情况工具之间的描述越相似错得越厉害。工具多了就做分组或者在系统提示里明确“优先使用某个工具”。4.3 手写执行循环完整可跑的骨架LangChain 提供了create_tool_calling_agent这类封装但我建议第一遍一定手写循环写通了再考虑用封装。手写的过程能让你彻底搞明白消息是怎么流转的出问题时也知道该看哪里。from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage tools [query_weather] tool_map {t.name: t for t in tools} llm_with_tools strict_llm.bind_tools(tools) def run_with_tools(question: str, max_rounds: int 4) - str: messages [ SystemMessage(content你是一个助手。需要外部数据时调用工具不要凭记忆编造。), HumanMessage(contentquestion), ] for _ in range(max_rounds): ai_msg llm_with_tools.invoke(messages) messages.append(ai_msg) if not ai_msg.tool_calls: return ai_msg.content for call in ai_msg.tool_calls: fn tool_map.get(call[name]) if fn is None: messages.append(ToolMessage( contentf错误不存在名为 {call[name]} 的工具, tool_call_idcall[id], )) continue try: result fn.invoke(call) except Exception as exc: result f工具执行失败{exc} messages.append(ToolMessage( contentstr(result), tool_call_idcall[id], )) return 抱歉工具调用轮次超限未能完成任务。这几十行里有几个点必须对齐错一个就跑不通。tool_call_id必须原样回填。每个tool_call都有一个id字段你返回的ToolMessage必须带上同一个 id模型才能把“结果”和“请求”对应起来。手动拼字符串、或者用旧的function_call格式都会导致模型看不懂上下文。工具执行一定要包 try/except。工具里可能有网络请求、文件读写、除零任何异常直接抛出去都会让整个链断掉。捕获之后把错误信息作为工具结果返回给模型是更好的选择模型看到“参数格式错误”往往能自己修正重试。必须设最大轮次。模型有时候会陷入循环反复调用同一个工具尤其是工具总是返回空结果或者错误的时候。卡在 4 轮超出就返回兜底文案这是最省事的保护。fn.invoke(call)这个写法是 LangChain 的便捷方式它接受完整的 tool_call 字典内部会自动取args。如果想更明确也可以写成fn.invoke(call[args])效果一样。系统提示里那句“不要凭记忆编造”不是摆设。加上之后模型在该调工具时的触发率明显上升尤其是问实时数据天气、股价、时间这类问题时不加这句它更倾向于直接编一个。4.4 多工具路由与轮次控制两个以上工具之后问题从“调不调”变成“调哪个”。这时候工具描述之间的区分度就是生死线。假设你有一个search_web和一个search_local_docs描述都写“搜索信息”模型只能靠猜把前者写成“搜索互联网上的实时公开信息适合新闻、时事”后者写成“搜索本地知识库文档适合公司制度、产品手册”路由准确率立刻不一样。多工具场景下还有一个现实问题串行执行慢。模型一次可能返回多个tool_calls这些调用之间如果没有依赖关系完全可以并发跑。用concurrent.futures包一层就行from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers4) as pool: futures {pool.submit(fn.invoke, call): call for call in ai_msg.tool_calls} for future in futures: call futures[future] try: content str(future.result()) except Exception as exc: content f工具执行失败{exc} messages.append(ToolMessage(contentcontent, tool_call_idcall[id]))并发之后要注意消息顺序。虽然ToolMessage的对应关系靠 id 而不是顺序但为了让模型容易读最好按原始tool_calls的顺序追加。轮次控制上我一般设 3 到 4。一轮是一个“模型请求工具 工具返回结果”的完整周期。超过 4 轮还没收敛八成是工具返回值有问题或者问题本身超出了工具能力继续加轮次只是在烧算力。可以在达到上限时把中间的消息历史打出来看一眼通常一眼就能看出卡在哪。5. 常见问题与排查实录5.1 工具死活不触发这是最高频的问题排查顺序建议按下面的表走从便宜的检查开始。现象可能原因验证方法处理tool_calls恒为空模型不支持工具调用换 7B instruct 模型试换模型该调不调直接编答案docstring 太笼统手动看 schema 内容补场景描述偶尔调偶尔不调温度过高把 temperature 设 0 再试降到 0.2 以下多个工具时调错描述区分度不够对比两个工具描述写明各自适用场景调用了不存在的工具名模型幻觉打印tool_calls看名字循环里做名字校验排第一的永远是模型本身。我用同一个提示词在 3B 和 7B 上对比过3B 在该调工具的场景下大概一半时候会直接编答案7B 能到九成以上。如果你的模型是小参数量的先别调提示词换模型。第二常见的是bind_tools之后忘了真的用绑定后的实例。写成llm.invoke(...)而不是llm_with_tools.invoke(...)工具清单压根没发出去模型当然不会调。这个错误很低级但真的很常见尤其是代码重构之后。第三个容易被忽略的是历史消息污染。如果前面的对话里模型输出过一段假装调用的文本后面的轮次会受这个模式影响反复输出类似内容而不产生真正的tool_calls。排查时把完整的messages打出来看发现问题就截断历史重开一轮。5.2 JSON 解析失败的六种现场结构化输出失败的花样比想象中多我按出现频率列一下。第一种前后带解释文字。模型说“好的这是提取结果{...}希望对你有帮助”。对策是在提示词里双向约束还是不行就在解析前用正则抠出第一个{和最后一个}之间的部分。第二种被代码围栏包住。json 开头结尾。解析前做一次 strip判断是否以围栏开头是就去掉首尾两行再解析。第三种用了中文标点。title“某某”里的冒号引号全角json.loads直接报错。这个在中文场景下特别常见可以在解析前做一次标点替换把全角的换成半角。这个替换有风险如果字段值本身包含全角标点会被误伤所以只对结构字符做替换而且要放在兜底流程里不能作为主路径。第四种字段名对不上。schema 里是book_title模型给了title。PydanticOutputParser会报缺失字段JsonOutputParser则静默给你一个缺键的字典。对策是字段名尽量短、语义明显别用下划线复合词。第五种类型漂移。year定义成int模型给了2023年。这个在带单位的字段上高频出现解决方式是在Field的 description 里写死格式要求并在 Pydantic 模型上加field_validator做容错转换。第六种JSON 截断。生成到一半撞上num_predict上限括号没闭合。这个从报错信息上能看出来Expecting , delimiter出现在末尾附近把上限调大或者缩短输出要求就行。实操心得把每次解析失败时的原始文本落盘攒二十条之后你会发现失败模式高度集中通常两三个正则或者提示词调整就能覆盖九成。5.3 流式与工具调用的兼容问题流式和工具调用放一起会出问题原因是tool_calls在流式返回里是分片拼的函数的 id 在第一个 chunk名字可能在第二个参数是一点点拼起来的字符串。你在循环里直接读chunk.tool_calls拿到的往往是半截数据。正确的做法是先把所有 chunk 合并。LangChain 的AIMessageChunk支持运算符累加之后得到完整的消息full None for chunk in llm_with_tools.stream(messages): if full is None: full chunk else: full chunk if isinstance(chunk.content, str) and chunk.content: print(chunk.content, end, flushTrue) if full and full.tool_calls: # 这里再做工具执行 ...另一种做法是用astream_events它能区分内容事件和工具事件on_chat_model_stream里拿文本on_tool_start/on_tool_end里拿工具执行。这套机制更适合做进度展示比如在终端里显示“正在查询天气……”这样的状态行同时文本继续往外流。代价是 API 比较复杂事件类型多调试的时候容易看花眼建议先用累加 chunk 的方式跑通再考虑升级。还有个细节工具调用的中间轮次通常是没必要流式展示的模型第一轮返回的只是一个“我要调工具”的空内容流式打印出来什么都没有用户看到的是停顿。可以在第一轮静默处理只在最终回答那一轮流式输出。判断方式就是看full.tool_calls是否为空。5.4 显存、速度与上下文跑本地模型绕不开资源问题。常见的几个现象和对策输出突然变慢十倍通常是模型被换出了显存检查ollama ps看看是不是有别的进程占着卡回答到一半开始重复同一句话是生成进入循环降温度、设num_predict上限长时间没响应直接超时多半是num_ctx设太大加上长上下文导致 prompt 处理时间爆掉——7B 模型处理 32K 上下文的 prompt光预填充就可能要几十秒用户以为是卡死。速度方面影响最大的是模型大小和量化其次是显存带宽。同一个 7B Q4 模型在不同机器上 token 生成速度能差三倍以上。如果只是做原型验证不用纠结速度先跑通再说如果要给人用考虑在应用层加个进度提示哪怕只是三个点。上下文管理上长期对话要注意消息列表会一直增长到后面每次请求都要把整段历史重发一遍prompt 处理时间线性上升。我的做法是保留最近 N 轮完整消息更早的做摘要压缩或者干脆截断。工具调用的历史消息尤其占地方因为工具返回值往往很长可以在追加ToolMessage之前把结果截断到合理长度比如 2000 字符超出部分打个省略标记。6. 落地时的一些取舍写到这里两个主题的链路都通了最后聊几句取舍上的体会。渲染和解析别混在一条链上。我见过有人在一个函数里既想流式打印 Markdown又想同时解析出结构化数据结果两个目标互相干扰代码复杂度翻倍。分开做对话走一套数据抽取走另一套各自用最适合的参数和提示词维护起来清爽得多。工具的粒度宁粗勿细。一开始很容易把每个小功能都做成一个工具结果工具列表膨胀到十几个模型路由准确率断崖下跌。更好的做法是把相关的操作合并成一个带action参数的函数比如manage_file(action, path, content)让模型只决定“做什么类别的事”细节参数在函数内部处理。兜底逻辑要写在前面。工具执行失败怎么办、JSON 解析失败怎么办、轮次超限怎么办这三个分支如果不提前设计等到线上出问题再补往往会引发更复杂的连锁反应。我的一般做法是每个分支都给一句明确的用户可读提示同时在日志里落下完整上下文方便事后复盘。别迷信大参数。14B 比 7B 强但不代表所有任务都值得上 14B。单工具调用这个场景7B 已经足够稳多花一倍显存换来的边际收益很小。真正需要往上堆参数的场景是多工具路由和复杂的多步推理那才是规模的用武之地。我个人在实际操作中的体会是本地模型跑工具调用这件事八成的效果来自三件事——模型选对、工具描述写清楚、参数压到低温度。剩下的两成才是框架封装和代码技巧。很多时候跑不通不是 LangChain 的问题也不是 Ollama 的问题回头看一眼 docstring 和工具清单问题往往就在那儿。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑