ARTICLE DETAIL

资讯详情

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

Agent工程化落地:从Demo到生产环境的七道关卡

Agent工程化落地:从Demo到生产环境的七道关卡 1. 为什么“Agent开发”不是写个Prompt就能跑通的活儿我第一次在客户现场看到那个“智能销售助手”崩溃时它正把一份PDF报价单里的单价3890元当成年份2023年去查历史天气——结果返回了“该年份无气象数据请重试”。客户盯着屏幕看了三秒转头问我“你们这Agent是真懂业务还是只会瞎猜”那一刻我意识到所谓大模型Agent开发根本不是把LLM API调通、再套个ReAct模板就完事的事。它是一整套工程化思维的迁移从“让模型回答问题”转向“让系统在不确定环境中持续做正确决策”。这个转变背后藏着三个被新手普遍低估的硬门槛。第一是状态管理失控LangChain默认的ConversationBufferMemory在多轮对话中会把用户说的“不要上个月的数据”和“把上个月销量导出为Excel”混成一句指令导致Agent反复执行错误动作第二是工具调用链断裂当Agent需要先查库存、再比价、最后生成比价报告时中间任何一个API超时或字段缺失整个流程就卡死而默认配置连重试机制都没有第三是意图漂移无感知用户从“帮我分析Q3销售趋势”突然跳到“把刚才的图表发给张总”Agent若没设计显式的状态机就会用Q3数据去生成邮件而不是提取刚生成的图表文件路径。这些坑恰恰是热搜词里反复出现“LangChain入门”“LangGraph教程”“Agent框架哪个好”的真实原因——大家卡在了“能跑通Demo”和“能交付生产”的断层带上。而真正拉开差距的从来不是谁先调通了OpenAI API而是谁先搞懂Agent不是模型的延伸而是模型与现实世界之间的协议翻译器。它要理解业务规则比如“销售数据必须脱敏后才能外发”要处理网络抖动比如ERP接口偶尔503还要在用户改口时及时回滚比如“等等别发邮件先加个附件”。这些能力没有一个藏在pip install langchain的文档里。所以这篇内容不讲“如何安装LangChain”也不列“十大Agent框架对比表”。我要带你拆解的是一个能扛住真实业务压力的Agent它的骨架是怎么一节一节长出来的。从最底层的“决策原子”设计到中间层的“状态流转控制”再到顶层的“人机协作容错”全部基于我在电商、制造、金融三个行业落地17个Agent项目的真实经验。你不需要懂Rust或分布式系统但得明白为什么Tool类里要强制定义return_direct字段为什么StateGraph的节点必须带版本号以及——为什么你写的第一个Agent Demo大概率会在第37次用户交互时突然开始胡言乱语。2. Agent的“决策原子”工具封装不是写个函数签名那么简单很多人以为Agent的工具Tool就是把API包装成函数比如get_weather(city: str) - dict。但我在给某汽车零部件厂做设备故障诊断Agent时发现当工程师输入“查看2024年Q2所有压铸机的停机记录”Agent调用fetch_maintenance_logs工具后返回的JSON里downtime_minutes字段在30%的记录中是空字符串。结果Agent直接把这个空值传给后续的统计模块导致整个报表的平均停机时长算成0——而实际是237分钟。问题出在哪出在工具封装的契约意识缺失。真正的工具不是函数而是带SLA服务等级协议的微型服务。它必须明确回答三个问题输入是否合法输出是否可信失败时如何降级LangChain的BaseTool类之所以要求重写_run方法而非直接暴露函数正是为了强制开发者思考这三点。2.1 输入校验用业务规则代替类型注解看一个典型反例# ❌ 危险写法仅依赖Pydantic类型检查 class SearchTool(BaseTool): name web_search description Search the web for information def _run(self, query: str) - str: # 直接调用搜索引擎API return search_engine(query)这段代码的问题在于query: str只保证了参数是字符串但业务上“查询”可能有硬性约束——比如某金融Agent规定涉及股票代码的查询必须是6位纯数字如600519而用户输入“贵州茅台股价”就会触发下游解析失败。正确的做法是把业务规则编译进校验逻辑# ✅ 生产级写法嵌入领域规则 class StockPriceTool(BaseTool): name stock_price description Get real-time stock price by stock code (6-digit number) def _run(self, query: str) - str: # 1. 业务规则校验必须是6位数字 if not re.match(r^\d{6}$, query.strip()): # 2. 主动纠错尝试从自然语言中提取 extracted self._extract_stock_code(query) if not extracted: return Error: Please provide a 6-digit stock code (e.g., 600519) query extracted # 3. 调用真实API try: data self._call_market_api(query) return fStock {query} current price: ¥{data[price]} except TimeoutError: return Error: Market data service unavailable. Try again later.这里的关键是_extract_stock_code不是简单的关键词匹配而是基于该厂历史工单数据训练的轻量NER模型仅2MB用ONNX Runtime部署专门识别“贵州茅台”“五粮液”等200个高频股票简称。这种设计让工具具备了上下文感知的纠错能力——当用户说“查下茅台股价”Agent不会报错而是自动转换为600519。提示工具校验层必须独立于LLM。我见过太多项目把“判断用户是否输入股票代码”交给LLM做结果模型把“600519.SH”识别为“600519”却把“sh600519”当成无效输入。规则引擎永远比概率模型更可靠。2.2 输出契约定义“可信结果”的边界条件另一个致命误区是认为工具返回JSON就等于数据可用。某物流Agent调用运单查询API时API文档写着“成功时返回status: success”但实测发现当快递员尚未揽收时API仍返回status: success只是tracking_info为空数组。如果Agent直接把这个“成功但无数据”的响应喂给LLM模型会强行编造一条运单轨迹——比如“已到达北京分拣中心”而实际上包裹还在发件人桌上。解决方案是引入输出契约Output Contractclass TrackingTool(BaseTool): name track_package description Track package status by waybill number def _run(self, waybill: str) - str: raw_resp self._call_api(waybill) # 契约检查必须包含有效轨迹点 if not raw_resp.get(tracking_info) or \ len(raw_resp[tracking_info]) 1: # 降级策略返回结构化错误而非原始空数据 return No tracking info available yet. Package may not be picked up. # 标准化输出统一时间格式、状态枚举 standardized { current_status: self._map_status(raw_resp[status]), last_update: datetime.fromtimestamp( raw_resp[update_time] ).strftime(%Y-%m-%d %H:%M), location: raw_resp[current_location] } return json.dumps(standardized, ensure_asciiFalse)这个设计让LLM永远只看到两种确定状态要么是标准化的JSON要么是明确的业务错误提示。避免了模型在模糊数据上“自由发挥”。2.3 失败熔断给每个工具配“保险丝”工具调用失败时90%的Demo代码选择try/except后返回空字符串。但在生产环境这会导致Agent进入无限循环——比如搜索工具超时Agent以为没结果于是重试重试又超时再重试……最终耗尽token预算。我的方案是在工具层内置熔断器Circuit Breakerfrom pydantic import BaseModel import time class ToolResult(BaseModel): success: bool content: str metadata: dict {} class RobustTool(BaseTool): # 熔断配置 failure_threshold: int 3 # 连续失败3次触发熔断 reset_timeout: int 60 # 60秒后自动重置 def __init__(self, **kwargs): super().__init__(**kwargs) self.failure_count 0 self.last_failure_time 0 def _run(self, *args, **kwargs) - str: # 检查熔断状态 if self._is_circuit_open(): return Tool temporarily unavailable. Please try later. try: result self._execute_tool(*args, **kwargs) self._on_success() return result except Exception as e: self._on_failure() return fTool execution failed: {str(e)} def _is_circuit_open(self) - bool: now time.time() if now - self.last_failure_time self.reset_timeout: self.failure_count 0 return self.failure_count self.failure_threshold这个熔断器带来的改变是质的当ERP接口连续3次超时Agent会主动切换到缓存数据模式比如返回上周的库存快照而不是卡死。用户得到的是“数据可能有延迟”而不是“系统无响应”。3. 状态机才是Agent的脊椎为什么LangGraph比LangChain更适合生产环境很多团队卡在LangChain的AgentExecutor上是因为它默认采用无状态决策流每次调用都重新加载记忆、重新解析历史就像一个人每次对话前都要翻一遍聊天记录。这在Demo里没问题但当用户说“把刚才生成的报表发给张总”Agent需要精确知道“刚才”指的是哪次操作、报表存在哪个临时路径、张总是谁——这些信息必须被结构化地维护。LangGraph的StateGraph解决了这个问题但它不是简单替换API而是要求你重构整个Agent的“骨骼”。我在为某银行做信贷审批Agent时把审批流程拆解为7个状态节点每个节点都是一个独立的决策单元状态节点触发条件执行动作状态跃迁validate_application收到新申请校验身份证号、收入证明格式→check_credit_score或 →reject_invalid_formatcheck_credit_score格式校验通过调用征信API设置超时熔断→assess_risk或 →wait_for_manual_reviewassess_risk征信数据返回LLM分析负债率、逾期记录→generate_approval_letter或 →request_additional_docs这个设计的关键在于状态本身携带上下文。比如assess_risk节点接收的state里不仅有用户提交的JSON还有credit_score_result来自上一节点、application_id全局唯一、reviewer_role当前审批人角色。LLM的提示词不再需要写“请根据用户提供的材料分析风险”而是直接写“请基于以下征信结果评估风险{credit_score_result}”。3.1 状态Schema设计拒绝“万能dict”拥抱强类型LangGraph要求定义State类很多人直接写# ❌ 反模式用dict承载一切 class State(TypedDict): messages: list memory: dict tool_calls: list这会导致两个问题一是LLM提示词里无法精准引用字段比如{memory[user_profile][annual_income]}容易拼错二是调试时难以追踪字段来源。我们的方案是按业务域拆分Schema# ✅ 领域驱动Schema from typing import Optional, List, Dict, Any from pydantic import BaseModel class ApplicationData(BaseModel): applicant_id: str annual_income: float employment_status: str class CreditReport(BaseModel): score: int overdue_months: int credit_limit: float class ApprovalState(BaseModel): application: ApplicationData credit_report: Optional[CreditReport] None risk_assessment: Optional[str] None approval_letter: Optional[str] None pending_actions: List[str] [] # 如 [upload_bank_statement] class Config: # 允许从dict初始化便于LangGraph兼容 extra ignore # LangGraph状态定义 class State(TypedDict): messages: Annotated[List[BaseMessage], add_messages] current_state: ApprovalState # 全局元数据 session_id: str timestamp: float这样做的好处是当Agent在assess_risk节点出错时我们能直接打印state.current_state.credit_report.score而不是在state[memory][credit_data]里翻找。更重要的是Pydantic的验证会在数据流入时自动拦截非法值——比如credit_report.score被设为-1会立刻抛出异常而不是让LLM拿到一个负数去计算。3.2 节点间数据流用“事件总线”替代“全局变量”传统做法是把所有数据塞进state然后每个节点都读写同一份内存。这在复杂流程中极易引发竞态比如generate_approval_letter节点正在写PDFsend_email节点就去读取文件路径结果读到空字符串。我们的解法是引入事件驱动的数据流每个节点只消费自己需要的输入产出明确的输出事件# 定义事件类型 class Event(BaseModel): type: str # CREDIT_CHECKED, RISK_ASSESSED, LETTER_GENERATED payload: Dict[str, Any] # 节点函数签名 def check_credit_score(state: State) - Dict[str, Any]: # 只读取application数据 app_data state[current_state].application credit_result call_credit_api(app_data.applicant_id) # 产出标准事件 return { events: [Event( typeCREDIT_CHECKED, payload{score: credit_result.score, overdue: credit_result.overdue_months} )], current_state: state[current_state].copy(update{ credit_report: CreditReport(**credit_result.dict()) }) } def assess_risk(state: State) - Dict[str, Any]: # 只消费CREDIT_CHECKED事件 credit_event next((e for e in state.get(events, []) if e.type CREDIT_CHECKED), None) if not credit_event: raise ValueError(Missing credit check result) # LLM分析逻辑... risk_text llm.invoke(fAssess risk based on score {credit_event.payload[score]}) return { events: [Event(typeRISK_ASSESSED, payload{text: risk_text})], current_state: state[current_state].copy(update{risk_assessment: risk_text}) }这种设计让节点彻底解耦assess_risk不关心信用分怎么来的只认CREDIT_CHECKED事件send_email节点只等待LETTER_GENERATED事件。当需要增加“人工复核”环节时只需插入一个监听RISK_ASSESSED事件的新节点完全不影响现有流程。3.3 状态持久化别让Redis成为你的单点故障生产环境的状态存储常被忽略。LangGraph默认用内存存储state重启就丢数据。有人直接上Redis结果遇到Redis集群脑裂时Agent状态错乱——比如审批流程卡在“等待补充材料”但Redis返回了旧的“已批准”状态。我们的方案是双写版本号校验class StateStore: def __init__(self, redis_client, pg_client): self.redis redis_client self.pg pg_client def save_state(self, session_id: str, state: State, version: int): # 1. Redis存最新状态低延迟读 redis_key fagent:state:{session_id} self.redis.setex(redis_key, 3600, json.dumps({ state: state.dict(), version: version, updated_at: time.time() })) # 2. PostgreSQL存历史快照防丢失 self.pg.execute( INSERT INTO agent_state_history (session_id, version, state_json) VALUES (%s, %s, %s), (session_id, version, json.dumps(state.dict())) ) def load_state(self, session_id: str) - Optional[State]: # 优先读Redis redis_data self.redis.get(fagent:state:{session_id}) if redis_data: data json.loads(redis_data) # 版本号校验防止Redis脏数据覆盖PG if self._is_version_consistent(session_id, data[version]): return State(**data[state]) # 回退到PG查最新快照 latest self.pg.fetch_one( SELECT state_json FROM agent_state_history WHERE session_id %s ORDER BY version DESC LIMIT 1, (session_id,) ) if latest: return State(**json.loads(latest[state_json])) return None这个设计让状态存储具备了最终一致性即使Redis全挂Agent也能从PG恢复而Redis的高并发读能力保证了实时性。版本号校验则杜绝了“Redis返回过期状态覆盖新状态”的经典问题。4. 容错控制当Agent开始胡言乱语时你靠什么把它拉回来所有Agent都会在某个时刻开始胡言乱语。不是因为模型坏了而是因为现实世界的输入永远超出训练数据的分布。某次给制造业客户部署设备巡检Agent用户上传了一张对焦模糊的电机铭牌照片Agent的OCR返回了乱码MOTR-8X2KLLM据此推理“这是8X2K型号电机”而实际型号是MOTOR-802K。结果Agent推荐了完全不匹配的维护方案。这时候靠重训模型或换更大参数量是徒劳的。真正有效的是构建一套分层容错体系像汽车的安全气囊一样在不同失效层级提供保护。4.1 第一层输入净化——在LLM看见之前就过滤噪声绝大多数胡言乱语源于垃圾输入。我们的输入净化管道包含三级过滤格式层拒绝非标准文件如.exe、.scr和超大文件50MB用python-magic库检测真实MIME类型而非依赖文件扩展名内容层对文本输入做敏感词扫描基于行业词典如医疗Agent禁用“治愈”“根治”等绝对化表述对图片调用轻量CV模型检测是否为文档/表格/人脸语义层用小模型做意图初筛——比如用户输入“帮我看看这个”系统先用TinyBERT判断这是“文档分析请求”还是“闲聊”若是后者直接返回预设话术绝不喂给主LLM。关键技巧净化规则必须可解释。当用户上传的PDF被拒返回的不是“输入不合法”而是“检测到加密PDF暂不支持解密请提供未加密版本”。这既降低了用户困惑也避免了因模糊提示导致的重复提交。4.2 第二层决策沙盒——让Agent在虚拟环境中试错最危险的胡言乱语发生在工具调用环节。比如Agent决定调用“发送邮件”工具但收件人字段填了张总company.com实际应为zhangcompany.com。如果直接执行会造成业务事故。我们的方案是引入决策沙盒Decision Sandboxclass SandboxExecutor: def __init__(self, tools: Dict[str, BaseTool]): self.tools tools def execute_sandbox(self, tool_name: str, tool_input: str) - Dict[str, Any]: # 1. 参数合法性检查不调用真实API if tool_name send_email: if not self._validate_email(tool_input): return {valid: False, error: Invalid email format} # 2. 模拟执行返回预设的沙盒响应 if tool_name search_web: return { valid: True, sandbox_response: Simulated search result for: tool_input[:20] } # 3. 真实调用仅当沙盒验证通过 try: result self.tools[tool_name]._run(tool_input) return {valid: True, real_response: result} except Exception as e: return {valid: False, error: str(e)} # 在Agent节点中使用 def execute_tool_node(state: State) - Dict[str, Any]: sandbox SandboxExecutor(tools) decision state[current_decision] # LLM输出的工具调用计划 # 先沙盒验证 sandbox_result sandbox.execute_sandbox( decision[tool], decision[input] ) if not sandbox_result[valid]: # 返回结构化错误触发重试或人工介入 return {messages: [AIMessage(contentfCannot execute {decision[tool]}: {sandbox_result[error]})]} # 沙盒通过执行真实调用 real_result sandbox_result[real_response] return {messages: [AIMessage(contentfExecuted {decision[tool]}: {real_result})]}这个沙盒让Agent在“决定做什么”和“实际做什么”之间插入一道安全阀。当收件人格式错误时Agent会收到明确的错误反馈而不是发一封失败的邮件。4.3 第三层输出护栏——用规则引擎给LLM戴上“紧箍咒”LLM的自由发挥是双刃剑。某次Agent生成合同条款时把“违约金不超过合同总额20%”写成了“违约金不超过合同总额200%”差了一个零。事后复盘发现LLM在生成数字时会受训练数据中高频数字如100、50影响产生幻觉。我们的输出护栏系统包含三道关卡数值范围锁对所有数字型输出强制校验是否在业务区间内。比如合同金额必须0且10亿利率必须在0.01%-24%之间逻辑一致性检查用规则引擎验证语句矛盾。例如当LLM输出“本合同自签订之日起生效”和“生效日期为2025年1月1日”时规则引擎会报警“生效时间冲突”法律条款白名单对合同、医疗等高危场景预置合规条款库。Agent生成的每句话必须能在白名单中找到匹配项否则标记为“需人工审核”。实现上我们用Drools规则引擎Java Python桥接规则示例// Drools规则合同金额校验 rule Contract Amount Validation when $c: Contract(amount 0 || amount 1000000000) then insertLogical(new ValidationError(Contract amount must be between 0 and 1B)); end // Drools规则生效日期冲突 rule Effective Date Conflict when $c: Contract(effectiveDate ! null effectiveDate ! immediately) $m: Message(content contains 签订之日起生效) then insertLogical(new ValidationError(Effective date conflict: specified date vs upon signing)); end这套护栏让LLM的输出不再是“黑箱结果”而是经过可验证的合规性审查。当规则触发时Agent会暂停流程向管理员推送待审任务而不是继续执行错误决策。5. 工程化落地从Demo到生产环境的七道关卡写完一个能跑通的Agent Demo只完成了10%的工作。剩下的90%是把它变成一个能7×24小时稳定运行的生产系统。我在交付第12个Agent项目时总结出必须跨过的七道工程关卡每一道都曾让我在凌晨三点被告警电话叫醒。5.1 关卡一Token预算的物理定律LLM的token不是无限资源而是有明确物理边界的消耗品。某次电商Agent在处理用户“对比iPhone15和华为Mate60的10个参数”请求时LLM生成了长达8000token的对比表格直接触发OpenAI的4096token限制返回context_length_exceeded错误。解决方案是Token预算的静态分配动态回收静态分配为每个Agent实例预设总预算如8192token其中4096token留给输入含历史4096token留给输出动态回收在messages列表中当累计token接近阈值时自动触发摘要压缩——不是简单删历史而是用专用摘要模型如facebook/bart-large-cnn将前10轮对话压缩为300token的语义摘要保留关键实体产品名、参数、用户偏好。关键技巧摘要模型必须微调。通用摘要模型会把“华为Mate60的卫星通话功能”压缩成“手机有通信功能”丢失关键差异点。我们用1000条电商对话微调后摘要准确率从62%提升到94%。5.2 关卡二工具调用的“交通管制”当Agent同时调用5个工具查库存、比价、生成报告、发邮件、更新CRM网络抖动会导致部分调用超时。如果所有工具都设30秒超时整个流程可能卡在第3个工具上而前2个工具的结果已过期。我们的“交通管制”策略分级超时核心工具如库存查询设5秒辅助工具如发邮件设15秒容错工具如日志上报设2秒异步流水线不等所有工具返回再汇总而是每个工具结果就绪即处理。比如库存数据一返回立即启动比价比价完成立即生成报告——形成流水线而非串行阻塞结果保鲜期为每个工具结果标注freshness_ttl如库存数据保鲜10分钟超过时限自动触发重查。这相当于给工具调用装上了红绿灯和ETC通道避免了“一个慢工具拖垮全局”的经典问题。5.3 关卡三状态同步的“最终一致性”多实例部署时用户可能在A实例发起请求B实例处理后续步骤。如果状态只存本地内存就会出现“用户在A实例说‘加急处理’B实例却按普通流程走”。我们的方案是状态变更事件化最终一致性每个状态变更如status: waiting_for_payment都发布为Kafka事件所有实例订阅该Topic收到事件后本地更新状态用RocksDB做本地状态缓存Kafka消息丢失时从RocksDB恢复最近状态。这样即使Kafka短暂不可用Agent也能用本地缓存继续服务只是状态同步延迟几秒——这对用户体验的影响远小于直接报错。5.4 关卡四监控的“可观测性三角”只监控CPU和内存是不够的。我们定义Agent的“可观测性三角”输入质量OCR准确率、语音识别WER、用户输入长度分布决策质量工具调用成功率、LLM输出合规率护栏触发次数、人工干预率业务质量任务完成率、平均处理时长、用户满意度NPS。关键指标示例指标告警阈值说明tool_call_failure_rate5%连续5分钟工具失败率超5%可能API故障llm_output_rejected_by_guardrail10%护栏频繁拦截说明LLM提示词需优化user_intervention_rate15%用户主动点击“转人工”比例过高流程设计有问题这些指标不是堆在Grafana里好看而是直接对接告警机器人——当tool_call_failure_rate超阈值自动创建Jira工单并通知运维。5.5 关卡五灰度发布的“渐进式信任”新Agent上线不能一刀切。我们的灰度策略分三阶段影子模式Shadow Mode新Agent与旧系统并行运行不执行任何操作只记录决策结果对比两者差异只读模式Read-Only Mode新Agent可生成结果但所有工具调用被拦截只返回模拟响应用户看到的是“预计结果”受限执行Limited Execution开放1%流量且所有工具调用需二次确认如“即将发送邮件给张总确认吗”。这个过程通常持续2-4周直到新Agent的user_intervention_rate低于旧系统才全量切换。5.6 关卡六灾难恢复的“一键回滚”Agent出问题时最怕的是“不知道改了哪行代码”。我们的CI/CD流水线强制要求每次部署生成唯一的agent_version如v2.3.1-20240520-1423所有状态存储Redis、PG的key都带版本号前缀回滚按钮不是重启服务而是切换Redis key前缀10秒内切回上一版。这意味着当新版本Agent开始胡言乱语运维只需点一下按钮所有用户瞬间回到稳定版本而无需排查代码、等待部署。5.7 关卡七成本治理的“账单可视化”LLM调用不是免费午餐。我们给每个Agent实例配“数字账单”实时显示本次会话的token消耗、API调用次数、耗时按业务线归集成本如“销售智能体”本月消耗$2,340设置成本阈值告警如单次会话超$5自动终止。这个账单不是财务部门的报表而是嵌入Agent界面的实时仪表盘。当用户发起复杂请求时Agent会主动提示“此操作预计消耗$3.2是否继续”——把成本意识下沉到用户端从源头控制浪费。6. 我的实战经验那些文档里永远不会写的真相写了这么多技术细节最后分享几个血泪教训——它们不会出现在LangChain官方文档里却是决定项目成败的关键。第一个真相别迷信“Auto-Router”很多框架宣传“自动路由到最优工具”实际是拿LLM当分类器。我们在测试中发现当工具数超过8个LLM的路由准确率从92%暴跌到63%。解决方案用轻量级规则引擎做初筛比如URL含/api/inventory就路由到库存工具LLM只处理规则无法覆盖的模糊case。这能让路由准确率稳定在98%以上且延迟降低70%。第二个真相记忆不是越多越好ConversationBufferMemory在长对话中会把“用户说不要折扣”和“用户说要最大折扣”记混。我们的经验是按意图切片存储。把一次完整的业务流程如“下单”作为独立记忆单元流程结束后归档新流程启用全新记忆。这样既保证上下文相关性又避免记忆污染。第三个真相提示词工程要“反向设计”不要从“我希望LLM说什么”开始写提示词而是从“我拿到什么输出才能交给下一个工具”倒推。比如send_email工具需要{to: xxx, subject: xxx, body: xxx}那就把提示词写成“请严格按JSON格式输出字段必须包含to、subject、body其中to必须是邮箱地址subject不能超过50字……”。用输出契约倒逼输入约束。第四个真相Agent的“人格”是运维成本给Agent设定“专业、友好”的人格听起来很酷但实际增加了10倍的测试成本——因为要验证每种语气下的业务准确性。我们的做法是剥离人格与逻辑。核心决策模块保持中性只输出结构化数据前端渲染层再添加语气如把{status: approved}渲染为“恭喜您的申请已获批”。这样逻辑升级不影响用户体验语气调整不牵扯业务。第五个真相安全不是加个防火墙Agent安全的核心是最小权限原则。我们给每个工具分配独立的API Key并按角色授权如客服Agent只能读订单不能改库存。曾经有个项目因共用KeyAgent被诱导执行了delete_all_orders命令——后来我们强制要求工具调用必须带permission_scope字段网关层实时校验。这些经验没有一条来自教程全部来自凌晨三点修复线上Bug的咖啡渍笔记。Agent开发的终极目标不是做出一个炫酷的Demo而是构建一个可预测、可审计、可运维的业务组件。当你能把一个Agent像数据库一样管理——知道它何时慢、为何错、怎么修、花多少钱——你才算真正入门。
返回列表