从LLM裸奔到工程化:Thin Harness与Fat Skills架构实战

📅 2026/8/26 10:09:44
从LLM裸奔到工程化:Thin Harness与Fat Skills架构实战
1. 项目概述从“裸奔”到“武装”的LLM应用范式转变最近在折腾AI应用开发的朋友估计都听过一个词——“LLM裸奔”。这可不是什么好词它形象地描绘了当前很多LLM应用的尴尬现状直接把一个庞大的语言模型比如GPT-4、Claude 3的API接口一接把用户问题原封不动地扔过去然后祈祷模型能“智能”地理解上下文、调用工具、处理好流程最后给出一个完美的答案。这就像让一个知识渊博但毫无准备的专家赤手空拳地去解决一个需要多种工具和流程的复杂问题结果往往是一地鸡毛——输出不稳定、逻辑混乱、无法可靠地使用外部工具或数据。我经历过太多次这种挫败。比如想让模型帮我分析一份财报并生成图表结果它要么开始一本正经地胡编数据要么反复尝试调用一个根本不存在的“画图函数”。问题的核心在于我们错误地把所有智能和决策的负担都压在了LLM这一个“大脑”上。而“Thin Harness, Fat Skills”正是对这种范式的一种反思和纠偏。它不是一个具体的框架或工具而是一种设计哲学和架构理念。简单来说“Harness”是那个轻量级的、负责流程控制和协调的“骨架”或“驾驶舱”而“Skills”则是那些功能具体、边界清晰、可以独立开发和测试的“肌肉”或“工具包”。我们的目标是让LLM这个“大脑”专注于它最擅长的理解、规划和决策而把具体的执行交给专门化的Skill。这种转变带来的好处是实实在在的。首先系统的可靠性大幅提升。一个专门用于查询数据库的Skill其代码逻辑是确定的返回的数据格式是规范的不会像LLM那样偶尔“灵光一现”或“胡言乱语”。其次开发和调试变得模块化。你可以单独为某个Skill编写单元测试而不用每次都启动整个复杂的Agent流程。最后它更符合软件工程的最佳实践如关注点分离、单一职责原则使得AI应用更容易维护、扩展和团队协作。接下来我们就深入拆解一下如何为你的LLM穿上这件得体的“外衣”。2. 核心理念拆解什么是Thin Harness与Fat Skills要理解这套架构我们得先抛开那些复杂的框架名词从最根本的软件设计思想入手。你可以把构建一个AI应用想象成组建一个特种作战小队。2.1 Thin Harness轻量级的指挥与控制中心“Harness”原意是马具或安全带在这里引申为“驾驭”或“控制”LLM的框架。所谓“Thin”是指这个框架本身应该尽可能简单、专注。它的核心职责不是去实现具体的业务逻辑而是扮演一个智能调度员和流程控制器。一个典型的Thin Harness需要完成以下几件事会话管理维护与用户的对话历史理解当前对话的上下文。这不仅仅是保存聊天记录还包括提取关键信息、识别用户意图的变化。意图路由与规划当用户提出一个请求如“帮我对比一下公司A和公司B过去三年的营收增长率并生成一个柱状图”时Harness需要解析这个请求并分解成一个可执行的计划。例如[调用Skill_A获取公司A财务数据] - [调用Skill_A获取公司B财务数据] - [调用Skill_B计算增长率] - [调用Skill_C生成图表描述] - [整合结果并回复用户]。工具Skill的封装与调用Harness需要以LLM能理解的方式通常是Function Calling的格式暴露所有可用的Skills。当LLM决定调用某个Skill时Harness负责将自然语言指令转换为具体的API调用执行它并将结构化的结果返回给LLM。错误处理与重试当某个Skill调用失败或LLM输出不符合预期时Harness需要有一套机制来捕获错误、决定重试策略例如重新规划、简化问题、向用户澄清。与LLM的交互这是Harness最“薄”的部分——它只需要负责格式化输入拼接系统指令、对话历史、工具描述和解析输出识别工具调用、提取纯文本回复。一个常见的误区是用LangChain或LlamaIndex这类功能丰富的框架直接当作Harness。它们当然可以但它们本身是“Fat”的内置了太多模块如各种记忆体、文档加载器、检索器。在Thin Harness理念下我们可能只使用它们最核心的AgentExecutor或StateGraph部分而将具体的工具实现完全剥离出去。2.2 Fat Skills功能具体、鲁棒性强的执行单元与“Thin”相对“Fat Skills”强调的是能力的具体和健壮。一个Skill应该像一个瑞士军刀上的单个工具——功能明确、接口清晰、自身足够可靠。一个好的Fat Skill具备以下特征单一职责一个Skill只做一件事并把它做好。例如get_stock_price(symbol: str) - float: 获取指定股票代码的实时价格。search_internal_wiki(query: str) - List[Document]: 在公司内部知识库中搜索。generate_bar_chart(data: Dict, title: str) - str: 根据提供的数据字典生成一个柱状图的描述或Base64编码的图片。强类型接口Skill的输入和输出必须是结构化的、强类型的。这不仅是给LLM看的Function Calling Schema更是给开发者和测试者看的。明确的接口契约能极大减少歧义。自包含的实现Skill内部应封装所有实现其功能所需的逻辑、依赖和错误处理。例如一个数据库查询Skill应该自己处理连接池、SQL拼接、查询超时和空结果集而不是把这些问题抛给Harness或LLM。可独立测试由于接口清晰、功能独立每个Skill都可以脱离整个AI应用进行单元测试和集成测试。你可以用模拟数据验证它在各种边界条件下的行为。“Fat”体现在哪里就体现在这个Skill的实现代码里。它可能包含复杂的业务逻辑、调用多个外部API、进行数据清洗和转换。它的“胖”是功能的充实而不是设计的臃肿。注意Thin Harness和Fat Skills的边界需要仔细界定。一个基本原则是所有非LLM相关的、确定性的逻辑都应该下沉到Skill中。LLM不应该去计算11等于几也不应该去拼接一个复杂的SQL语句这些都应该由对应的Skill来完成。3. 架构设计与核心组件选型理解了理念我们来看看如何落地。设计一个基于Thin Harness, Fat Skills的系统核心是定义好Harness与Skills之间、以及Harness与LLM之间的通信协议。3.1 核心架构图概念模型[用户输入] | v [Thin Harness] | (1. 管理会话生成包含工具描述的Prompt) v [LLM] | (2. 返回思考过程 工具调用请求 或 最终答案) v [Thin Harness] | |--- (3a. 如果是工具调用) --- [Fat Skill A] --(结果)--| |--- (3b. 如果是工具调用) --- [Fat Skill B] --(结果)--| |--- ... | | | v (4. 将工具结果整合重新提交给LLM) | [LLM] --------------------------------------------------| | (5. 生成下一步动作或最终回复) v [Thin Harness] | v [最终回复给用户]3.2 Harness的实现选型框架 vs 自研目前社区主要有两种路径路径一使用成熟框架的“轻量模式”LangChain Agent Custom Tools: LangChain的create_react_agent或create_openai_tools_agent提供了一个现成的Harness骨架。你只需要定义好Tools即我们的Skills并配置好AgentExecutor。关键在于要克制使用LangChain的其他“全家桶”功能把记忆、检索等能力也通过自定义Tool来实现保持Harness的“Thin”。LangGraph: 这是更灵活、更显式地定义控制流的选择。你可以用StateGraph清晰地描绘出Agent在不同状态如“等待用户输入”、“执行工具”、“评估结果”间的转换。它比传统的Agent更底层让你对流程有绝对控制权非常适合构建复杂的、有状态的多步骤工作流。微软AutoGen: AutoGen的AssistantAgent和UserProxyAgent对话模式本质上也是一种Harness。它擅长多Agent协作可以将不同的Skills分配给不同的Agent通过对话来协调任务。路径二基于LLM原生Function Calling自研这是最“Thin”的方式。你完全自己管理对话历史自己构建包含工具描述的Prompt直接调用OpenAI、Anthropic等提供的带有tools参数的Chat Completion API然后自己解析返回的tool_calls执行对应的函数再将结果以tool角色发回给API。这种方式自由度最高依赖最少但需要你手动处理更多细节如对话历史截断、工具调用循环控制。3.3 Skill的设计与封装无论Harness选择哪种Skill的设计是相通的。一个Skill通常包含三部分函数实现用Python或其他语言实现核心功能。Function Calling Schema一个符合OpenAI Function Calling格式的JSON描述包括name、description和parameters。description至关重要它直接决定了LLM是否能在正确场景下想起并调用这个Skill。错误处理与返回格式Skill必须处理内部可能发生的所有错误并以结构化的方式如JSON返回即使出错也要返回一个明确的错误信息字段方便Harness和LLM处理。实操心得Skill描述的“艺术”给Skill写描述(description)和参数说明(parameters.description)时要站在LLM的角度思考。不要只写“查询天气”要写“根据提供的城市名称查询该城市当前及未来几天的天气情况包括温度、湿度、天气状况和降水概率”。参数也要描述清楚比如city参数要说明“请输入完整的城市名称例如‘北京’、‘New York’不支持缩写”。4. 从零搭建一个实践示例智能数据分析助手光说不练假把式。我们以构建一个“智能数据分析助手”为例演示如何实践Thin Harness, Fat Skills。这个助手能接受用户如“分析公司销售数据.csv告诉我上个月销售额最高的产品类别是什么并用邮件把总结发给我经理”这样的复杂指令。4.1 定义Fat Skills我们先定义三个SkillsSkill 1: 数据加载与基本分析 (data_analyzer)功能加载CSV/Excel文件执行基本的Pandas操作如分组、聚合、排序、筛选。接口run_analysis(data_source: str, operation: str, args: dict) - dictSchema描述示例{ name: run_analysis, description: 对指定的数据文件进行基本的数据分析操作。支持加载本地CSV/Excel文件并执行如分组统计、排序、筛选等操作。, parameters: { type: object, properties: { data_source: {type: string, description: 数据文件的路径例如 sales_data.csv}, operation: {type: string, description: 要执行的操作可选值load仅加载、groupby按列分组聚合、sort排序、filter筛选}, args: {type: object, description: 操作所需的参数。例如对于groupby需要提供by分组列和aggregate聚合方式如sum, mean} }, required: [data_source, operation] } }Skill 2: 图表生成 (chart_generator)功能根据提供的数据和图表类型使用Matplotlib或Plotly生成图表并保存为图片或返回Base64。接口generate_chart(chart_type: str, data: dict, title: str, output_path: str None) - strSchema描述需详细说明支持的chart_type如‘bar’, ‘line’, ‘pie’以及data字段期望的格式如{‘x’: […], ‘y’: […], ‘labels’: […]}。Skill 3: 邮件发送 (email_sender)功能通过SMTP协议发送邮件支持附件。接口send_email(to: str, subject: str, body: str, attachment_path: str None) - bool4.2 实现Thin Harness以LangGraph为例我们使用LangGraph来构建Harness因为它能清晰表达我们想要的“规划-执行-评估”循环。from typing import TypedDict, Annotated, List import operator from langgraph.graph import StateGraph, END from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage, ToolMessage from langgraph.prebuilt import ToolNode # 1. 定义状态State class AgentState(TypedDict): messages: Annotated[List, operator.add] # 消息历史 user_query: str # 原始用户问题 analysis_result: dict # 存储中间分析结果 chart_path: str # 存储生成的图表路径 # 2. 初始化LLM和Tools(Skills) llm ChatOpenAI(modelgpt-4-turbo, temperature0) # 将我们定义的三个Skill函数绑定为LangChain Tools tools [data_analyzer_tool, chart_generator_tool, email_sender_tool] llm_with_tools llm.bind_tools(tools) # 3. 定义节点函数 def planner_node(state: AgentState): 规划节点分析用户请求决定调用哪个工具或直接回复 system_prompt f 你是一个数据分析助手。你可以使用工具来加载分析数据、生成图表和发送邮件。 用户的问题是{state[user_query]} 请逐步思考并决定下一步该做什么。如果你有足够信息回答就直接回答。否则调用合适的工具。 可用的工具{[tool.name for tool in tools]} messages [{role: system, content: system_prompt}] state[messages] response llm_with_tools.invoke(messages) state[messages].append(response) return state def tools_node(state: AgentState): 工具执行节点执行LLM请求的工具调用 last_message state[messages][-1] tool_calls last_message.tool_calls if not tool_calls: return state # 使用LangGraph预建的ToolNode来执行工具 tool_node ToolNode(tools) return tool_node.invoke(state) def evaluator_node(state: AgentState): 评估节点检查工具执行结果判断是否还需要继续 last_message state[messages][-1] # 如果上一条消息是工具执行结果且结果中包含最终答案所需的所有信息则转向结束 # 否则继续循环到规划节点 # 这里可以加入更复杂的逻辑比如检查是否已生成图表、是否已发送邮件等 if 分析完成 in str(last_message.content) and chart_path in state: # 认为任务完成可以结束 return state # 默认继续循环 return state # 4. 构建图 workflow StateGraph(AgentState) workflow.add_node(planner, planner_node) workflow.add_node(tools, tools_node) workflow.add_node(evaluator, evaluator_node) # 设置边 workflow.set_entry_point(planner) workflow.add_edge(planner, tools) # 规划完就去执行工具 workflow.add_edge(tools, evaluator) # 执行完就评估 workflow.add_conditional_edges( evaluator, # 根据评估结果决定下一步继续规划还是结束 lambda state: END if state.get(task_complete, False) else planner ) # 编译图 app workflow.compile()4.3 运行与迭代现在我们可以运行这个助手initial_state AgentState( messages[], user_query分析一下sales_q1.csv找出销售额最高的产品线并生成一个饼图最后把总结发到managercompany.com, analysis_result{}, chart_path ) final_state app.invoke(initial_state, config{recursion_limit: 10})在这个过程中HarnessLangGraph构建的工作流负责推动整个状态机前进而三个Fat Skill则各司其职完成具体的脏活累活。LLM只需要在planner_node中根据当前状态和可用工具做出“接下来该做什么”的决策。5. 避坑指南与进阶思考在实际操作中你会遇到很多框架文档里不会写的坑。这里分享几个我的深刻体会5.1 Skill设计的常见陷阱陷阱一Skill过于复杂如果一个Skill的参数超过5个或者描述超过200字就要警惕了。这通常意味着它做了太多事。考虑将其拆分成多个更细粒度的Skill。陷阱二错误信息不友好Skill内部抛出的异常一定要被捕获并转化为LLM能理解的、结构化的错误信息。比如不要返回Python的KeyError堆栈而是返回{error: true, message: 在数据中未找到指定的‘产品线’列请确认列名是否正确。}。陷阱三忽视Skill的执行可能有延迟如网络请求或副作用如发送邮件、写入数据库。Harness必须考虑超时设置对于有副作用的Skill如发邮件在执行前最好能通过LLM生成一个确认步骤或者由Harness提供一个“模拟执行”模式用于调试。5.2 提升LLM在Harness中的表现提供充足的上下文在每次调用LLM时除了当前对话还应将相关的、已成功执行的Skill结果摘要也放入上下文。这能帮助LLM更好地理解当前进展。设计好的系统提示词System Prompt这是Harness的“灵魂”。好的提示词要明确告诉LLM你的角色是什么。你可以使用哪些工具每个工具是干什么的这里直接引用Skill的description。你必须遵循的思考流程例如鼓励使用ReAct模式的“Thought/Action/Action Input/Observation”。输出的格式要求。处理LLM的“幻觉”调用LLM有时会调用一个不存在的工具或者用错误的参数格式调用工具。Harness需要有能力拦截这些无效调用并给LLM一个友好的错误反馈引导它重新思考。5.3 监控、评估与持续改进一个健壮的AI应用离不开监控。你需要记录LLM调用日志输入、输出、Token消耗。Skill调用日志输入参数、执行结果、耗时、是否出错。用户交互链路完整的用户会话包括所有的中间步骤。基于这些日志你可以分析技能使用频率哪些Skill最常用哪些很少被用到这指导你优化Skill设计。LLM规划成功率LLM制定的计划有多少比例被成功执行失败点在哪儿用户满意度结合人工评估或简单的用户反馈如点赞/点踩持续迭代你的Skills描述和Harness的提示词。从“LLM裸奔”到“Thin Harness, Fat Skills”本质上是从“相信一个全能魔法黑盒”到“构建一个可靠工程系统”的思维转变。这条路开始可能会觉得更繁琐需要你设计接口、编写具体的业务逻辑、处理各种边界情况。但当你看到你的AI应用能稳定、可靠地处理复杂任务并且每个部分都清晰可测、可维护时你会明白这一切都是值得的。这不再是玩票式的Prompt工程而是真正意义上的AI软件工程。