
1. 为什么我要从零搭一个HiAgent第一次听到HiAgent这个词是在一个做企业效率工具的朋友群里。有人丢了一张截图说他们内部用HiAgent把客服工单的自动分类准确率从七成拉到了九成以上而且整个搭建过程只用了不到两周。我当时的第一反应是又一个套壳的智能体编排平台但仔细看完他们的流程之后我发现事情没那么简单。HiAgent本质上是一个智能体Agent框架它的核心定位是让大语言模型LLM能够真正“听懂”人的指令并且按照预设的流程去调用工具、查询数据、生成结果。和市面上很多只做对话包装的方案不同HiAgent更强调指令解析、任务编排和上下文管理这三件事的配合。换句话说它解决的不是“让模型说人话”而是“让模型干人事”。这篇文章适合三类人看第一类是对智能体开发感兴趣但还没动手写过一行Agent代码的开发者第二类是用过一些低代码智能体平台但觉得灵活性不够、想深入理解底层机制的技术人员第三类是需要在业务系统里嵌入智能体能力但不确定从哪个框架切入的架构师。我会从框架选型、核心概念、实操搭建、常见问题四个维度把HiAgent的实战路径完整拆一遍。需要提前说明的是HiAgent目前并没有一个绝对统一的官方标准版本不同团队基于相似理念做了各自的实现。我下面讲的内容是基于我在实际项目中搭建的一套可运行方案结合了社区里常见的实践模式。如果你用的是某个具体的商业版本细节上会有差异但核心思路是通的。2. 智能体框架选型为什么是HiAgent而不是别的2.1 先搞清楚Agent和普通LLM调用的区别很多人第一次接触智能体会觉得不就是给LLM加个系统提示词让它按照格式输出吗这个理解只对了一半。普通的LLM调用是“一问一答”你给一段文本它返回一段文本。而Agent的核心在于“自主决策加工具调用”。举个例子。你问普通LLM“帮我查一下上个月华东区的销售数据然后生成一份简报。”它只能根据训练数据里的知识编一段话因为它没有连接你的数据库。但Agent可以做到先解析你的指令识别出“查数据”和“生成简报”两个子任务然后调用数据库查询工具拿到真实数据再把数据传给LLM做总结最后按照指定格式输出。这中间的差别就是HiAgent这类框架要解决的问题。它需要管理任务分解、工具注册、上下文传递、结果校验这一整套流程。2.2 HiAgent的核心设计理念我选择HiAgent作为切入点主要是因为它在这几个方面做得比较平衡指令解析层HiAgent不会直接把用户输入丢给LLM而是先经过一个轻量的意图识别模块。这个模块可以基于规则也可以基于小模型目的是把模糊的自然语言指令映射到预定义的任务类型上。这样做的好处是降低了对LLM的依赖即使模型能力一般也能保证基本的路由正确。工具抽象层在HiAgent里每个外部能力都被封装成一个“工具”Tool有明确的名称、描述、输入参数和输出格式。LLM不需要知道工具内部怎么实现只需要知道什么时候调用哪个工具、传什么参数。这个设计让系统扩展变得很简单加一个新工具就是注册一个函数的事。上下文管理器多轮对话和复杂任务里上下文会越来越长。HiAgent的做法是分层管理短期上下文保留最近几轮对话长期上下文通过摘要和向量检索来维护。这样既不会爆token也不会丢失关键信息。执行引擎这是最核心的部分。它负责按照任务图依次执行节点处理节点之间的依赖关系并在某个节点失败时决定是重试、跳过还是终止。我见过很多自己手写的Agent脚本最后都卡在执行流程的健壮性上而HiAgent把这部分做成了可配置的。2.3 和其他方案的对比维度HiAgent纯Prompt方案低代码平台灵活性高可自定义每个环节低受限于模型能力中受平台限制开发成本中需要写代码低低可维护性高模块清晰差提示词一改就崩中工具集成强支持自定义工具无有限适合场景复杂业务流程简单问答快速验证这个对比不是要贬低其他方案而是想说如果你的需求只是做一个FAQ机器人那没必要上HiAgent。但如果你要处理的是多步骤、多工具、有状态的任务HiAgent这类框架的优势就会非常明显。3. 核心概念拆解Agent、LLM、Embedding到底怎么配合3.1 LLM在HiAgent里扮演什么角色LLM在HiAgent里不是“大脑”而是“翻译官加推理器”。它主要负责三件事把用户指令翻译成结构化的任务描述、在多个工具之间做选择、把工具返回的结果翻译成人类可读的回复。这意味着你不需要一个特别大的模型。我实测下来7B到13B参数量的模型在指令遵循能力尚可的情况下配合好的提示词和工具描述就能跑出不错的效果。当然如果任务复杂度高还是得上更大的模型。3.2 Embedding和向量检索的位置Embedding在HiAgent里主要用在两个地方一是长期记忆的检索二是工具描述的语义匹配。当工具数量很多的时候不可能把所有工具的描述都塞进提示词里。这时候就需要用Embedding把用户指令和工具描述都向量化先做一轮粗筛再把最相关的几个工具传给LLM做精排。这里有个坑要注意Embedding模型和LLM最好是配套的或者至少是在相似语料上训练的。我试过用中文Embedding配英文LLM检索效果明显下降。3.3 工具调用的完整链路一次完整的工具调用在HiAgent里是这样的用户输入指令意图识别模块判断是否需要调用工具如果需要从工具库中检索候选工具LLM根据候选工具的描述决定调用哪个、传什么参数执行引擎调用对应工具拿到结果结果经过格式化后返回给LLMLLM生成最终回复这个链路里第4步是最容易出问题的。LLM可能会选错工具或者参数格式不对。HiAgent的解决办法是在工具描述里写清楚参数类型和示例同时在执行前加一层参数校验。4. 从零搭建一个能跑的HiAgent4.1 环境准备和依赖安装我用的技术栈是Python 3.10以上核心依赖包括pip install openai1.0.0 pip install pydantic2.0 pip install numpy pip install faiss-cpu pip install fastapi pip install uvicorn如果你要用本地模型还需要装transformers和torch。我建议先用API版本的LLM跑通流程再考虑本地部署。项目目录结构大概是这样hiagent-demo/ ├── config/ │ └── settings.py ├── core/ │ ├── agent.py │ ├── executor.py │ └── context.py ├── tools/ │ ├── base.py │ ├── database.py │ └── calculator.py ├── memory/ │ └── vector_store.py └── main.py这个结构不复杂但每个模块的职责要分清楚。core放核心逻辑tools放工具实现memory放记忆管理config放配置。4.2 定义工具基类所有工具都继承同一个基类这样执行引擎可以用统一的方式调用它们。from abc import ABC, abstractmethod from pydantic import BaseModel class ToolInput(BaseModel): pass class ToolOutput(BaseModel): result: str success: bool class BaseTool(ABC): name: str description: str input_schema: type[ToolInput] abstractmethod def run(self, input_data: ToolInput) - ToolOutput: pass def to_prompt(self) - str: return f工具名{self.name}\n描述{self.description}\n参数{self.input_schema.schema()}这个基类里to_prompt方法很关键。它把工具的信息格式化成LLM能理解的文本。参数用Pydantic的schema自动生成省得手写。4.3 实现一个数据库查询工具假设我们要查销售数据工具可以这样写class SalesQueryInput(ToolInput): region: str month: str class SalesQueryTool(BaseTool): name sales_query description 查询指定区域和月份的销售数据 input_schema SalesQueryInput def run(self, input_data: SalesQueryInput) - ToolOutput: # 实际项目中这里连数据库 mock_data { (华东, 2024-01): 1200000, (华南, 2024-01): 980000, } key (input_data.region, input_data.month) if key in mock_data: return ToolOutput(resultstr(mock_data[key]), successTrue) return ToolOutput(result未找到数据, successFalse)注意这里的description要写得足够清楚LLM就是靠这个来判断什么时候该调用这个工具的。我见过有人把描述写成“查询数据”结果LLM根本分不清该用哪个工具。4.4 上下文管理器的实现上下文管理器要解决两个问题一是控制token长度二是保留关键信息。class ContextManager: def __init__(self, max_tokens4000): self.max_tokens max_tokens self.short_term [] self.long_term_summary def add_message(self, role, content): self.short_term.append({role: role, content: content}) self._compress_if_needed() def _compress_if_needed(self): total sum(len(m[content]) for m in self.short_term) if total self.max_tokens * 0.8: # 把最早的一半对话做摘要 old self.short_term[:len(self.short_term)//2] self.long_term_summary self._summarize(old) self.short_term self.short_term[len(self.short_term)//2:] def get_context(self): return self.long_term_summary, self.short_term这个实现比较粗糙但核心思路是对的短期上下文保留原文长期上下文做摘要。实际项目中摘要可以用LLM来做也可以用规则提取关键实体。4.5 执行引擎的核心逻辑执行引擎负责把LLM的输出解析成工具调用然后执行。import json class Executor: def __init__(self, tools: list[BaseTool], llm_client): self.tools {t.name: t for t in tools} self.llm llm_client def execute(self, user_input: str, context: ContextManager): # 第一步让LLM决定是否调用工具 tool_descriptions \n.join([t.to_prompt() for t in self.tools.values()]) prompt f你可以使用以下工具 {tool_descriptions} 用户指令{user_input} 如果需要调用工具请按以下JSON格式输出 {{tool: 工具名, params: {{参数名: 参数值}}}} 如果不需要调用工具直接回复用户即可。 response self.llm.chat(prompt) # 第二步解析LLM输出 try: parsed json.loads(response) if tool in parsed: tool self.tools.get(parsed[tool]) if tool: input_data tool.input_schema(**parsed[params]) result tool.run(input_data) # 把结果返回给LLM生成最终回复 final_prompt f工具返回结果{result.result}\n请根据这个结果回复用户。 return self.llm.chat(final_prompt) except json.JSONDecodeError: pass return response这段代码里有个关键点LLM返回的JSON不一定可靠。我试过很多次模型会在JSON前后加解释文字或者参数名写错。所以实际项目中一定要加异常处理和重试机制。5. 实操中踩过的坑和排查技巧5.1 LLM返回的JSON格式不稳定怎么办这是最常见的问题。模型有时候返回纯JSON有时候在JSON外面包一层json有时候参数名用中文有时候干脆返回一段自然语言。我的解决办法是三层防护第一层在提示词里明确要求“只输出JSON不要有任何其他文字”。第二层用正则表达式提取JSON部分兼容json包裹的情况。第三层如果解析失败把错误信息返回给LLM让它重新生成。import re def extract_json(text): # 尝试直接解析 try: return json.loads(text) except: pass # 尝试提取代码块中的JSON match re.search(r(?:json)?\s*(\{.*?\})\s*, text, re.DOTALL) if match: try: return json.loads(match.group(1)) except: pass # 尝试提取第一个大括号对 match re.search(r\{.*\}, text, re.DOTALL) if match: try: return json.loads(match.group(0)) except: pass return None这个函数我用了很久能解决九成以上的格式问题。剩下的一成基本是模型能力问题换个模型就好了。5.2 工具选错或者参数传错LLM选错工具通常是因为工具描述不够区分度。比如你有“查询订单”和“查询物流”两个工具描述都写“查询相关信息”那模型肯定懵。解决办法是在描述里加入否定信息。比如“查询订单”的描述写成“查询订单的金额、状态不包含物流信息”。这样模型就能区分了。参数传错的话最常见的是日期格式。用户说“上个月”模型可能传“2024-01”也可能传“上月”。我的做法是在工具内部做兼容处理同时把标准格式写在参数描述里。5.3 上下文太长导致响应变慢当对话轮次多了之后每次请求都带着全部历史token消耗会线性增长。除了前面说的摘要压缩还有一个技巧是只保留与当前任务相关的历史。具体做法是给每条消息打标签比如“销售查询”“售后问题”检索的时候只取同标签的历史。这个在HiAgent里可以通过扩展ContextManager来实现。5.4 常见问题速查表问题现象可能原因排查方向解决方案LLM不调用工具工具描述不清晰检查description是否具体补充使用场景和示例调用工具但参数为空参数schema太复杂简化参数结构减少嵌套用扁平结构返回结果乱码编码问题检查工具输出编码统一用UTF-8响应时间超过10秒上下文过长统计token数量启用摘要压缩多轮对话后失忆上下文被截断检查压缩策略调整保留轮数6. 进阶让HiAgent更懂你的业务6.1 用少量样本微调意图识别如果你的业务指令比较固定可以用少量标注数据微调一个小模型来做意图识别。这样比纯靠LLM判断更稳定也更省token。我试过用200条标注数据微调一个BERT分类模型意图识别准确率能到95%以上而且推理速度比LLM快一个数量级。6.2 工具编排的两种模式HiAgent支持两种工具编排模式串行和并行。串行就是前一个工具的输出作为后一个工具的输入适合有依赖关系的任务。并行是多个工具同时执行适合独立子任务。比如“查销售数据并查库存数据”这两个就可以并行。实现上串行用队列并行用线程池。注意并行的时候要做好结果合并和异常处理。6.3 加入人工审核节点在关键业务场景里完全自动化的风险比较高。HiAgent可以在执行流程里插入人工审核节点比如工具返回结果后先推送给人工确认确认后再继续。这个功能在客服、财务审批场景里特别有用。实现方式是在执行引擎里加一个wait_for_approval状态配合外部消息队列。7. 我个人的一些实操体会搭完这套东西之后我最大的感受是智能体框架的难点不在LLM而在工程。模型能力每年都在涨但任务分解、上下文管理、异常处理这些工程问题是需要一个个踩坑踩出来的。另外不要一开始就追求大而全。我见过有人上来就想做一个能处理所有业务的通用Agent结果三个月都没跑通。正确的做法是先选一个具体的、边界清晰的任务比如“查询销售数据并生成简报”把这个场景跑通跑稳再逐步扩展。还有一个细节工具的描述文档要当成产品文档来写。你写得多清楚LLM就能用得多准确。我现在的习惯是每加一个工具先写描述再写实现。描述写不清楚说明这个工具的设计本身就有问题。最后分享一个调试技巧把每次LLM的输入输出都记日志包括完整的提示词和返回内容。出问题的时候翻日志比猜原因快得多。我用的就是简单的文件日志按天切分够用了。