ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

大模型应用的“灯与精灵”:Agent 工程化核心链路与最佳实践

大模型应用的“灯与精灵”:Agent 工程化核心链路与最佳实践 如果只看表面“The Lamp and the Genie”像是一个童话故事的标题一盏旧油灯一个无所不能的精灵摩擦灯身就能呼唤奇迹。但在 AI 工程化语境里这个隐喻恰好戳中了很多开发团队的真实状态。大模型就是那个“精灵”——它看起来什么都会能写代码、能推理、能总结文档。然而真正决定应用能不能落地的从来不是精灵本身有多强而是你手里那盏“灯”做得怎么样你有没有给它一套清晰可用的工具接口有没有把上下文管理好有没有在它乱来的时候兜住底线这篇文章想讨论的正是“灯”的工程实现。如果你正在做 Agent、AI 助手、自动化工作流或者只是想把 LLM 接入现有业务系统这篇文章会从架构分层、Function Calling 链路、记忆管理、权限边界到排错方法完整拆解一个可运行的“灯与精灵”系统应该怎么搭。读完你不仅能跑通一个最小示例还能知道把项目推到生产环境时真正容易踩的坑在哪里。1. 从“灯与精灵”说起AI 应用的真实瓶颈先说一个经常被误解的事实在绝大多数 Agent 项目里模型能力并不是瓶颈。很多团队第一次接触 LLM 应用开发时以为核心工作就是选一个强模型、写一段好 Prompt然后调 API。但真正进入开发后会发现最大的工作量几乎都花在了模型之外如何把业务能力封装成模型能理解的“工具”模型决定调用某个工具时参数怎么传才可靠多轮对话中历史上下文怎么存、怎么截断、怎么压缩模型偶尔会乱调用工具或产生幻觉系统要怎么拦截和回退工具执行失败时错误信息怎么反馈给模型让它自己修正这些问题和模型智商无关它们属于工程问题。“The Lamp and the Genie”这个隐喻的价值就在这里。精灵是模型灯是围绕模型搭建的整套系统。灯包括工具协议、执行环境、记忆机制、权限控制、日志追踪和失败兜底。灯做得越稳精灵的能力才能被约束在正确范围内释放灯做得粗糙再强的模型也会在真实业务里翻车。如果只把注意力放在“哪个模型更强”上就好比只关心精灵的法力值却忽略了灯本身可能漏油、冒烟、甚至把精灵放出来捣乱。这也是本文想把“灯与精灵”落到工程层面的原因。接下来我们先建立一套 Agent 系统的架构认知再用代码逐步实现它。2. Agent 的核心架构接口层、能力层与记忆层在动手写代码之前建议先在心里建立一张 Agent 系统的分层图。很多项目前期跑得很快后期维护困难就是因为所有逻辑都堆在一个 Python 文件里没有分层意识。一个相对清晰的 Agent 系统至少包含三层。2.1 接口层用户怎么触达“灯”接口层是用户与系统交互的入口。它可以是命令行脚本、Web 对话框、企业微信机器人、Slack 应用也可以是内部管理后台的一个表单。接口层负责两件事接收用户输入返回最终结果。它不应该包含业务逻辑。用户说“帮我查一下明天的天气”接口层只需要把这句话传给 Agent 核心然后把 Agent 返回的结果展示出来。接口层最容易被忽视的是“流式输出”和“状态回传”。如果你的 Agent 执行链路很长——比如要先查数据库、再调外部 API、最后生成报告——用户会等待很久。此时接口层最好能支持流式输出或者至少给用户一个进度反馈而不是让页面卡住十秒钟。2.2 能力层精灵可以召唤哪些“法术”能力层是 Agent 能调用的所有工具集。它对应 Function Calling 中的 tool 定义也对应业务系统的实际执行函数。这里的关键不是“工具越多越好”而是“工具边界越清晰越好”。举例来说一个电商客服 Agent 可以有以下工具查询订单状态查询退款进度修改收货地址转接人工客服查询商品库存每个工具都需要定义名称、描述、参数结构。模型会根据用户请求和工具描述自行决定调用哪个工具。这就像你给精灵一本法术书它自己判断什么时候该用哪个法术。2.3 记忆层精灵如何“记住”上下文LLM 本身是无状态的。它每次收到请求都像第一次见到你。所谓记忆完全是工程手段模拟出来的。记忆层设计通常分三步短期记忆当前会话内的多轮对话历史。长期记忆跨会话的用户偏好、历史订单、业务实体信息。工作记忆当前正在执行的任务上下文比如工具调用中间结果。很多项目一开始只做了短期记忆就是简单地把 messages 数组越塞越长。等 token 成本上来之后才开始考虑摘要压缩和向量检索。我的建议是记忆方案在系统设计第一天就要想清楚不然后期改造的代价远高于一开始就做分层。2.4 三者如何协作一个完整的请求流程是这样的接口层收到用户输入转成 message 对象。Agent 核心把用户输入、系统提示词、可用的工具定义、历史记忆一起发给 LLM。LLM 返回两种结果之一要么直接给出文本回复要么返回一个 tool_calls 请求表示“我需要调用某个工具参数已填好”。如果是后者能力层执行对应工具把结果转成 tool message 回传给 LLM。循环第 2 步直到 LLM 不再请求调用工具输出最终回复。接口层把最终回复展示给用户同时更新记忆层。这套流程通常被称为“Agent Loop”或“ReAct Loop”。它是所有 LLM Agent 应用的骨架。后面的代码示例会完整实现这个循环。3. 环境准备与模型选型在写代码之前先确认一下运行环境。3.1 基础环境本文示例使用 Python 3.10依赖管理使用 pip。理论上 Python 3.9 也可以运行但建议使用较新版本避免类型注解和语法兼容问题。需要安装的核心依赖是openaiSDK。不过要注意目前市面上大多数兼容 OpenAI 协议的服务都可以用同一套 SDK 对接只需要修改base_url即可。如果你的项目使用的是国内大模型服务或其他兼容接口不影响本文的代码逻辑。# 建议在虚拟环境中安装 pip install openai python-dotenv如果你需要做向量记忆或摘要可能还会用到pip install numpy redis这些依赖在后续记忆章节会用到。3.2 模型选型建议模型选型没有绝对标准但可以根据场景做一个基础判断如果 Agent 要处理复杂推理、多步骤工具调用优先选推理能力强的商用大模型。如果业务数据敏感、必须私有化部署选择本地模型或私有化服务但需要接受工具调用能力的下降。如果只是做简单问答、文本分类不需要复杂的 Function Calling轻量模型就够。如果涉及中文场景要关注模型对中文工具描述的理解能力有些模型在中文工具选择上明显更稳。从工程角度我的建议是不要一开始就追求最强模型而是先用一个中等偏上的模型把链路跑通再逐步升级模型。因为链路不同最终效果差异很大。先保证流程可靠再谈效果优化。3.3 环境变量管理不要把 API Key 写死在代码里。推荐使用.env文件管理# 文件路径.env OPENAI_API_KEYyour-api-key-here OPENAI_BASE_URLhttps://api.example.com/v1 MODEL_NAMEgpt-4o-mini# 文件路径config.py import os from dotenv import load_dotenv load_dotenv() OPENAI_API_KEY os.getenv(OPENAI_API_KEY) OPENAI_BASE_URL os.getenv(OPENAI_BASE_URL) MODEL_NAME os.getenv(MODEL_NAME, gpt-4o-mini)强调一点如果你的服务商有特殊的环境变量命名方式以你实际拿到的文档为准。上面的写法是用 OpenAI 兼容接口时的通用做法。4. 做一个最小“精灵”Function Calling 链路实现这一章是全文核心。我们会实现一个完整的最小 Agent它至少包含一个模拟查询天气的工具。一个能理解工具定义、决定是否调用工具的 LLM 客户端。一个完整的 Agent Loop处理多轮工具调用。4.1 定义工具函数先写一个真实的 Python 函数。为了演示方便这里用真实请求第三方天气 API 会引入不可控因素所以我们用 mock 数据模拟但函数签名和真实项目一致。# 文件路径tools/weather.py from datetime import datetime def get_weather(city: str, date: str None) - dict: 模拟查询指定城市在指定日期的天气情况。 真实项目里可以替换为第三方天气 API 的调用。 if date is None: date datetime.now().strftime(%Y-%m-%d) # 演示数据实际项目请替换为真实业务逻辑 mock_data { city: city, date: date, weather: 晴, temperature: 24°C ~ 32°C, humidity: 45%, wind: 东南风 3级, tips: 适合户外活动注意防晒, } return mock_data这个函数很简单但它代表了能力层最基本的单元一个输入参数明确、返回值结构化的 Python 函数。4.2 定义 Tool SchemaFunction Calling 的核心是让模型知道“有哪些工具可用以及每个工具的参数长什么样”。模型不会直接读你的 Python 函数它读的是你提供的 JSON Schema。{ type: function, function: { name: get_weather, description: 查询指定城市在指定日期的天气情况。如果不提供日期默认查询当天。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, date: { type: string, description: 日期格式为 YYYY-MM-DD可选 } }, required: [city] } } }这个 Schema 有几个细节值得注意name必须与 Python 函数名完全一致否则模型可能调用一个不存在的函数。description要写清楚工具的作用和参数含义。模型就是靠 description 来判断该不该调用、怎么填参数的。description 写得太模糊模型就会乱用。required字段标记哪些参数必须提供。如果你的函数有可选的 date 参数就不要放进 required。把上面的 JSON 保存为 Python 字典在调用模型时直接传给tools参数。4.3 实现 Agent Loop现在写核心的 Agent 循环。这个循环做的事情是把用户消息和系统提示词发送给模型。检查模型返回是普通文本回复还是工具调用请求。如果是工具调用执行对应的 Python 函数把结果以 tool 消息发回给模型。重复直到模型不再请求调用工具。# 文件路径agent.py import json from openai import OpenAI from config import OPENAI_API_KEY, OPENAI_BASE_URL, MODEL_NAME client OpenAI( api_keyOPENAI_API_KEY, base_urlOPENAI_BASE_URL, ) tools [ { type: function, function: { name: get_weather, description: 查询指定城市在指定日期的天气情况。如果不提供日期默认查询当天。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, date: { type: string, description: 日期格式为 YYYY-MM-DD可选 } }, required: [city] } } } ] # 工具名称到实际 Python 函数的映射表 TOOL_FUNCTIONS { get_weather: get_weather, } def run_agent(user_input: str, messages: list None) - (str, list): 运行 Agent 主循环。 返回 (最终文本回复, 更新后的 messages 列表) if messages is None: messages [] messages.append({role: user, content: user_input}) # 最大调用轮次防止模型无限循环 MAX_ITERATIONS 5 for _ in range(MAX_ITERATIONS): response client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolstools, ) message response.choices[0].message # 模型没有要求调用工具说明可以直接返回 if not message.tool_calls: messages.append({role: assistant, content: message.content}) return message.content, messages # 模型要求调用工具把 assistant 消息加入历史 messages.append({ role: assistant, content: message.content, tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments, } } for tc in message.tool_calls ] }) # 依次执行工具调用 for tc in message.tool_calls: function_name tc.function.name function_args json.loads(tc.function.arguments) print(f[Agent] 调用工具: {function_name}, 参数: {function_args}) if function_name in TOOL_FUNCTIONS: result TOOL_FUNCTIONS[function_name](**function_args) else: result {error: f未找到工具: {function_name}} messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) # 超出最大轮次后返回一个提示信息 return 执行超过最大轮次请稍后再试或简化指令。, messages if __name__ __main__: reply, history run_agent(北京明天天气怎么样) print([Agent] 最终回复:, reply)4.4 代码逻辑拆解这段代码是整个 Agent 系统的最小骨架我们来逐段解释关键逻辑。第一处关键点是messages.append。在 Function Calling 中messages数组不只是“对话历史”它本身也是一个“状态机”。模型根据 messages 中 assistant 的 tool_calls 声明和 tool 的执行结果决定下一步动作。如果漏掉了 assistant 消息或者漏掉了 tool 消息模型就会“失忆”无法继续推理。第二处关键点是 tool_calls 的格式。SDK 返回的message.tool_calls是包含多个调用的列表。一个模型可以在一次回复中请求调用多个工具比如同时查天气和查航班。你的循环要支持这种批量调用而不是假设一次只调用一个工具。第三处关键点是json.dumps(result, ensure_asciiFalse)。工具执行结果必须以字符串形式传入 tool message。这里使用ensure_asciiFalse是为了保证中文可读避免变成\uXXXX转义形式否则模型可能会误解内容。第四处关键点是MAX_ITERATIONS。没有这个限制模型有可能陷入无限循环——比如它反复调用同一个工具却始终不生成最终回复。再加一个轮次限制是一个工程兜底同时提醒你如果经常触及上限说明工具定义或 Prompt 设计有问题。4.5 运行与验证安装依赖、填写 API Key 之后运行python agent.py预期输出大致如下[Agent] 调用工具: get_weather, 参数: {city: 北京, date: 2025-...}如果一切正常返回结果应该包含天气信息。如果模型没有调用工具而是直接回复“我无法查询天气”优先检查API Key 是否有权限访问 tools。模型是否支持 Function Calling。tools参数是否在请求中正确传递。这部分跑通后你已经拥有一个完整的最小 Agent 系统。接下来要解决的是“让精灵记住你说过的话”。5. 记忆与上下文管理如何让精灵不“失忆”很多 Agent 项目在单轮对话时表现很好一到多轮对话就崩。原因很简单每一轮都把所有历史消息原封不动地发给模型导致上下文越来越长token 成本越来越高模型反而被无关历史干扰。5.1 最简单的方式会话内消息累积把上一章的run_agent返回的messages保存下来在下一轮传入就是最简单的记忆。reply1, history run_agent(北京明天天气怎么样) print(reply1) reply2, history run_agent(那上海呢, history) print(reply2)第二句“那上海呢”没有提到天气但模型结合历史记录能理解用户是在问上海的天气。这就是短期记忆的基本形态。5.2 上下文太长怎么办滑动窗口messages 无限增长会带来两个问题token 费用飙升、模型注意力被稀释。一个通用做法是“滑动窗口 摘要”。先看滑动窗口的简单实现# 文件路径memory/sliding_window.py from collections import deque class SlidingWindowMemory: def __init__(self, max_messages: int 20): self.messages deque(maxlenmax_messages) def add(self, message: dict): self.messages.append(message) def get_messages(self) - list: return list(self.messages) def clear(self): self.messages.clear()这里的maxlen会保证超过 20 条后自动丢弃最旧的消息。适用于大多数普通对话场景。5.3 更优雅的方式摘要压缩滑动窗口的问题是很久以前的用户意图可能会被直接丢掉。如果用户在第 1 轮说过“帮我订北京到上海的机票”到第 15 轮说“把时间改成后天”丢弃历史会导致模型不知道“时间”指的是什么。这时就需要摘要压缩。思路是当消息超过阈值时把较早的消息交给 LLM 生成一段摘要作为系统提示词的一部分保留下来。# 文件路径memory/summary.py from openai import OpenAI from config import OPENAI_API_KEY, OPENAI_BASE_URL, MODEL_NAME client OpenAI( api_keyOPENAI_API_KEY, base_urlOPENAI_BASE_URL, ) def summarize_messages(messages: list) - str: 将一段消息历史压缩成简洁摘要。 history_text \n.join( f{m[role]}: {m.get(content, )} for m in messages ) prompt ( 请将以下对话历史压缩成一段简洁摘要保留用户的意图、 关键实体人名、地名、时间、订单号等和待办事项。\n\n f{history_text} ) resp client.chat.completions.create( modelMODEL_NAME, messages[ {role: system, content: 你是对话摘要助手只输出摘要正文。}, {role: user, content: prompt}, ], ) return resp.choices[0].message.content使用摘要的方式是设置一个历史消息上限比如 30 条。当超过上限时把前 20 条交给摘要函数生成 summary。保留 summary 为一条 system message丢弃前 20 条原始消息。继续后续对话。这种方式在长对话场景下稳定性和成本控制都更好。缺点是摘要本身要消耗一次模型调用所以不要每轮都做只在消息数达到阈值时触发。5.4 长期记忆用向量库还是数据库如果你的 Agent 需要跨会话记住用户偏好比如“用户每次订酒店都喜欢高层房间”这已经超出 messages 数组能管的范围需要引入长期记忆。常见做法有两种结构化存储把用户偏好抽成键值对或数据库字段比如用户 ID、偏好标签。适合规则明确的信息。向量存储把历史对话或用户描述向量化在需要时检索 top-k 相关片段注入系统提示词。适合模糊的、非结构化的信息。对于大多数业务系统优先用结构化存储。只有当用户描述本身千人千面、难以抽字段时再考虑向量库。实现长期记忆时不要把它和短期记忆混在一起。推荐的结构是short_term_messages当前会话的 messages 数组。long_term_profile从历史中提取出的用户画像注入 system prompt。relevant_memories从向量库检索到的与当前问题相关的历史片段。三种记忆各司其职模型拿到的是一份结构清晰的“记忆包”而不是一坨混乱的文本。6. 安全、权限与失败兜底不让精灵“乱来”模型不可控是 Agent 系统与普通后端系统最大的区别。普通函数的调用链是程序员写死的Agent 的调用链是模型现场决定的。这意味着你的代码必须假设模型一定会犯错。6.1 所有危险操作都要人工确认如果 Agent 可以调用的工具包括“删除订单”“转账”“发送邮件”“修改数据库”那么一定要设计人工确认环节。一个典型的做法是“两阶段提交”Agent 生成工具调用请求进入“待确认列表”。系统拦截执行把调用参数展示给用户或管理员。用户确认后才真正执行工具。实现时只需要在 Agent Loop 中加入一个确认函数def confirm_action(function_name: str, arguments: dict) - bool: 危险操作前进行人工确认。 dangerous_tools {delete_order, refund, send_email, transfer} if function_name in dangerous_tools: print(f确认操作: {function_name}, 参数: {arguments}) result input(是否确认执行? (y/n): ) return result.strip().lower() y return True然后在执行工具前调用if not confirm_action(function_name, function_args): messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps({error: 用户取消执行此操作}, ensure_asciiFalse), }) continue这样模型会收到“用户取消”的 tool 结果能够理解用户的意图并重新规划回复。这在体验上比简单报错要自然得多。6.2 工具侧要做参数白名单校验即使模型已经提供了参数代码也不能直接无脑透传。应该在工具入口做校验。以get_weather为例def get_weather(city: str, date: str None) - dict: ALLOWED_CITIES {北京, 上海, 广州, 深圳} if city not in ALLOWED_CITIES: return {error: f暂不支持该城市: {city}} # ... 后续逻辑如果你的业务允许用户输入任意城市这可能不适用。但如果是订单查询、用户信息查询等敏感操作必须做用户身份校验数据权限校验频率限制参数合法性校验不要让模型提供什么参数系统就执行什么参数。模型的输出永远被视为“不可信输入”。6.3 对模型输出做严格解析模型返回的tool_calls中arguments是字符串形式的 JSON。虽然大多数模型会生成合法 JSON但你不能假设 100% 合法。建议用安全解析函数替代直接json.loadsimport json def safe_json_loads(text: str) - dict: 解析工具参数 JSON。如果解析失败返回空字典。 if not text: return {} try: return json.loads(text) except json.JSONDecodeError: # 这里可以尝试用正则抽取参数 return {}如果解析失败就把错误信息回传给模型让它重新生成合法的 JSON。这比直接抛出异常要好。6.4 超时、重试和熔断真实工具调用可能很慢可能超时也可能暂时不可用。Agent 系统需要面向失败设计对每个工具调用设置超时时间。对可重试的失败网络抖动做有限次重试。对连续失败的第三方服务做熔断不要无限重试。工具异常时把异常信息转换成模型能理解的 tool 消息尝试让模型重新规划。一个简单的超时封装import signal from functools import wraps def timeout_handler(signum, frame): raise TimeoutError(工具调用超时) def with_timeout(seconds: int): def decorator(func): wraps(func) def wrapper(*args, **kwargs): signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(seconds) try: return func(*args, **kwargs) finally: signal.alarm(0) return wrapper return decorator with_timeout(10) def slow_tool(): # 模拟可能超时的外部调用 import time time.sleep(15) return result不过需要注意signal方案只适用于 Unix 环境。如果部署在 Windows 或异步环境中可以考虑用asyncio.wait_for或线程池实现。6.5 日志与可观测性Agent 系统的排错比传统后端难很多因为你不能只靠堆栈信息判断问题。强烈建议记录以下内容每次 LLM 请求的输入输出messages 的关键内容、tools 列表。模型选择了哪个工具、填了什么参数、是否修改过。每次工具执行的耗时、结果、错误信息。当前轮次、总轮次、token 消耗。系统提示词版本。这些日志在开发和灰度阶段几乎能解决 90% 的玄学问题。7. 完整示例一个带记忆和校验的“灯与精灵”系统把前面几章的内容整合起来提供一个更完整的示例。7.1 项目结构lamp-and-genie/ ├── .env ├── config.py ├── agent.py ├── tools/ │ ├── __init__.py │ └── weather.py ├── memory/ │ ├── __init__.py │ ├── sliding_window.py │ └── summary.py └── main.py7.2 主程序入口# 文件路径main.py from agent import run_agent from memory.sliding_window import SlidingWindowMemory def main(): memory SlidingWindowMemory(max_messages20) print(灯与精灵 Agent 已启动输入内容开始对话输入 exit 退出。) while True: user_input input(\n你: ) if user_input.strip().lower() exit: break history memory.get_messages() reply, history run_agent(user_input, history) # 更新记忆 memory.clear() for msg in history: memory.add(msg) print(f\n精灵: {reply}) if __name__ __main__: main()这个主程序把用户输入传给 Agent然后把更新后的 messages 写回滑动窗口记忆。它展示了一个最小可用的“接口层 能力层 记忆层”组合。7.3 运行效果python main.py你可以尝试以下对话序列你: 北京明天天气怎么样 精灵: 调用 get_weather 后返回天气信息 你: 那上海呢 精灵: 结合历史知道用户还在问天气调用 get_weather 查询上海对比上一章的单轮示例这里的差异在于第二句“那上海呢”如果单独发给模型模型很可能回复“上海是中国的直辖市您想了解什么呢”——因为它不知道用户背景。但有了历史消息模型就能正确推断意图。这就是记忆层的价值。7.4 测试建议给刚搭好的系统准备一组“黄金用例”正常工具调用用户明确表达要查天气。省略参数用户说“帮我查一下天气”没有说城市。模型应该反问城市而不是猜一个。多轮省略第二句只提城市模型应该结合上文补全日期。无关问题用户问“今天股市怎么样”模型应该直接回答而不是强行调用天气工具。危险操作如果接入确认机制测试用户取消场景下模型能否自然回应。这组用例可以作为后续每次修改 Prompt 或工具定义后的回归测试。8. 常见问题与排查思路Agent 系统的问题排查本质上是“模拟模型的思考过程”。以下表格列出高频问题。问题现象可能原因排查方式解决方案模型从不调用工具tools 参数未正确传递或模型不支持 Function Calling打印请求参数确认 tools 字段存在检查模型文档是否支持工具调用升级支持 Function Calling 的模型检查请求体格式模型调用不存在的函数工具映射表缺函数或 Schema 中 name 与代码不一致检查 TOOL_FUNCTIONS 字典检查 tools 定义中的 name统一工具注册表用同一个常量作为函数名和 Schema name工具参数 JSON 解析失败模型生成了非法 JSON或参数值包含特殊字符打印 tc.function.arguments 原始字符串使用 safe_json_loads把解析失败信息回传给模型重新生成上下文越来越长费用飙升没有滑动窗口或摘要机制统计每轮 messages 长度和 token 消耗引入 SlidingWindowMemory 或摘要压缩多轮对话后忘记关键信息早期消息被滑动窗口丢弃检查消息被丢弃的时间点观察模型是否缺少关键实体对重要信息做结构化存储不要只依赖 messages工具执行很慢第三方接口慢或网络问题查看工具耗时日志设置超时和重试异步执行工具考虑缓存模型循环调用工具不输出最终结果工具返回结果无法支撑模型得出结论或 Prompt 指示不清打印每轮 tool_calls 和 tool 结果增加 MAX_ITERATIONS 限制改进工具返回信息调整系统提示词模型在无关问题上也调用工具工具 description 写得太泛或系统提示词没约束查看实际调用场景收紧 description在系统提示词中明确“只在用户询问相关信息时才调用工具”中文参数乱码或转义json.dumps 未设置 ensure_asciiFalse或数据库编码问题检查 tool 消息中的 content 编码统一使用 ensure_asciiFalse数据库连接使用 utf8mb4这些问题的共同特征是大多数情况下问题不在模型而在工程侧。所以在排查时先从工具定义、消息格式、上下文管理入手最后才考虑换模型。9. 工程化最佳实践一个能跑通 demo 的 Agent 和一套能上生产的 Agent 系统差距非常大。这一章总结几条真正有用的工程建议。9.1 使用统一工具注册表不要在哪里用到工具就写死哪里。建议做一个统一的注册表集中管理所有工具的名称、Schema、实现函数、权限级别。# 文件路径tools/registry.py from tools.weather import get_weather TOOL_REGISTRY { get_weather: { function: get_weather, permission: user, timeout: 5, }, # delete_order: {...} } def get_tools_schema() - list: 根据注册表自动生成 tools 参数列表 # 实际实现可以从每个工具的 metadata 生成 JSON Schema pass好处是新工具只需在注册表加一行后续做权限控制、超时配置、监控统计都有统一入口。9.2 工具描述要写“什么时候该用”很多开发者写工具描述时只写“这个工具是做什么的”没有写“什么时候应该用”。结果模型很容易在错误场景下调用工具。更好的描述方式{ name: get_weather, description: 当用户询问某城市天气、温度、降雨概率、风力等问题时调用本工具查询实时天气。如果用户没有指定城市请先反问用户。 }这段描述同时说了适用场景和反例场景模型的选择会更准确。9.3 日志要记录“决策过程”不只是结果普通后端需要错误堆栈Agent 系统更需要的是“决策轨迹”用户说了什么、模型怎么想、选了哪个工具、参数是什么、工具返回了什么。建议至少用 JSON Lines 格式记录到本地日志或日志平台{timestamp: ..., session_id: ..., event: llm_request, messages_preview: ..., tool_choice: get_weather}有了决策轨迹你才能在模型表现异常时还原现场。9.4 工具的错误信息要让模型能“看懂”工具函数返回错误时不要只返回类似{error: 500}这样的信息。模型需要知道发生了什么、下一步应该怎么办。推荐错误信息格式{ error: 天气服务暂时不可用, suggestion: 请稍后重试或者询问用户是否接受查看昨天的天气数据 }模型读到 suggestion 之后可以直接把它转化成对用户的话术体验会好很多。9.5 灰度发布新加一个工具、修改一个 Prompt、升级一个模型都可能影响 Agent 的整体行为。强烈建议先在小流量节点上跑对比工具调用准确率。准备好回滚方案旧工具版本、旧 Prompt、旧模型。每次只改一个变量不要同时改工具定义和模型版本。用黄金用例集做回归测试。Agent 系统的行为具备一定随机性不回归测试就上线很容易出现“今天好好的明天全部乱掉”的情况。10. 最后说回“灯与精灵”回到文章开头的比喻。如果你从零搭过一个 Agent 项目大概率会有这种体会真正耗费心力的不是选择哪个模型而是把灯本身修好——工具协议怎么定义、上下文怎么管理、失败怎么兜底、权限怎么控制、日志怎么留痕。这些工作不像模型能力那么引人注目但它们决定了应用能不能稳定跑下去。模型会一代比一代强今天的精灵可能明天就退役了。但只要你的“灯”设计得足够好——工具注册清晰、记忆分层可靠、安全边界牢固、可观测性完备——那么更换底层模型只是一次配置变更而不是项目重写。反过来如果灯的设计一团糟哪怕换一个更强的精灵也只会让错误发生得更快、更复杂。所以我的建议是先从一个小工具集开始把一个最小链路跑通然后把记忆、权限、日志这些工程细节逐步加进来。等灯足够牢固再考虑让精灵去做更复杂的事。这篇文章里的代码覆盖了 Function Calling、Agent Loop、滑动窗口记忆、工具校验、安全确认和日志思路足够支撑一个最小可用的 Agent 脚手架。建议收藏备用结合你自己的业务场景把这盏灯点亮。如果你想继续深入下一步可以从这几个方向选一个研究更复杂的多工具协作编排把记忆系统升级为向量检索或者把 Tools 从单一函数扩展到 GraphQL、数据库查询等真实业务能力。每一条路都会遇到新的问题但骨架已经在这里了。
返回列表