news 2026/9/8 16:54:55

【Day 16】把公司制度文档喂给Agent,它能替HR回答问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Day 16】把公司制度文档喂给Agent,它能替HR回答问题

摘要

员工反复问年假、报销,HR成人肉FAQ。本文用RAG+Function Calling+多轮记忆搭文档问答:文档自动分块入ChromaDB,Agent自主决定何时检索,回答标来源,追问懂上下文。


先看一段对话:

👤 年假有几天? 🤖 入职满1年5天,满3年10天,满5年15天。 📌 出自:公司制度FAQ.txt 第4段 👤 能顺延吗? 🤖 年假当年未休完可顺延至次年3月31日。 📌 出自:公司制度FAQ.txt 第4段 👤 出差住宿呢? 🤖 一线城市500元/晚,其他城市400元/晚。 📌 出自:公司制度FAQ.txt 第6段

注意第二轮"能顺延吗"——没有主语、没有关键词,但系统知道问的是年假。这不是关键词匹配能做到的。

这个系统整合了前面学过的三样东西:RAG(Day07-08)负责找文档,Function Calling(Day02)负责让Agent自己决定什么时候搜,多轮记忆(Day05)负责理解"能顺延吗"指什么。

系统怎么工作的

需要查文档

打招呼等

用户提问

LLM分析问题

search_documents工具

直接回答

ChromaDB
向量检索

拼接检索结果+对话历史

LLM生成回答+标注来源

追问?带上历史再来一轮

跟Day07朴素RAG的区别是:Agent自己决定要不要搜。用户说"你好",LLM不调用搜索工具直接回复;用户问"年假几天",LLM自动调用search_documents(query="年假天数")。这个决策由Function Calling完成,不需要写if-else判断。

文档入库:分块是关键

文档上传时做三件事:读取文本、按500字符切块(带50字符重叠)、调GLM embedding-3生成向量存入ChromaDB。

importchromadbfromopenaiimportOpenAIclassEmbeddingFn:"""GLM向量嵌入,ChromaDB调用这个类把文本转向量"""def__init__(self):self.client=OpenAI(api_key=os.environ["GLM_API_KEY"],base_url="https://open.bigmodel.cn/api/paas/v4/")self.model="embedding-3"def__call__(self,input):ifisinstance(input,str):input=[input]vecs=[]foriinrange(0,len(input),64):resp=self.client.embeddings.create(model=self.model,input=input[i:i+64])vecs.extend(d.embeddingfordinresp.data)returnvecsclassDocStore:def__init__(self,path="./chroma_db"):client=chromadb.PersistentClient(path=path)self.col=client.get_or_create_collection("docs",embedding_function=EmbeddingFn())defadd(self,text,source):chunks=self._chunk(text)self.col.add(documents=chunks,ids=[f"{source}__{i}"foriinrange(len(chunks))],metadatas=[{"source":source,"idx":i}foriinrange(len(chunks))])returnlen(chunks)def_chunk(self,text,size=500,overlap=50):chunks=[]start=0whilestart<len(text):chunks.append(text[start:start+size].strip())ifstart+size>=len(text):breakstart+=size-overlap# 回退overlap字符,避免截断句子returnchunksdefsearch(self,query,n=3):r=self.col.query(query_texts=[query],n_results=n)returnlist(zip(r["documents"][0],r["metadatas"][0]))

分块时50字符的重叠区是为了防止一句话被切成两半。比如"年假满1年享有5天"如果被切成"年假满1年"和"享有5天"两块,检索哪块都可能语义不完整。重叠区保证关键句至少完整出现在一个块里。

每个块带着sourceidx元数据,回答时就能标注"出自哪个文件第几段"。

Agent:Function Calling驱动检索

Agent的核心是一个循环——LLM决定调工具,执行后把结果喂回去,直到LLM给出最终回答:

importjsonfromllm_clientimportLLMClient TOOLS=[{"type":"function","function":{"name":"search_documents","description":"搜索公司制度文档,返回相关片段。当用户问到公司制度、福利、流程等问题时调用。","parameters":{"type":"object","properties":{"query":{"type":"string","description":"检索关键词,简洁明确"}},"required":["query"]}}}]SYSTEM="""你是公司制度问答助手。根据检索到的文档片段回答问题。 如果检索结果不相关或不充分,直接说没找到,不要编造答案。 回答时在末尾标注引用来源。"""classDocQAAgent:def__init__(self):self.llm=LLMClient(provider="glm")self.store=DocStore()self.history=[]defask(self,question):messages=[{"role":"system","content":SYSTEM}]messages.extend(self.history[-10:])# 最近10轮对话messages.append({"role":"user","content":question})sources=[]for_inrange(3):# 最多循环3次resp=self.llm.client.chat.completions.create(model=self.llm.model,messages=messages,tools=TOOLS,temperature=0.3)msg=resp.choices[0].messageifnotmsg.tool_calls:# LLM给出最终回答answer=msg.content self.history.append({"role":"user","content":question})self.history.append({"role":"assistant","content":answer})returnanswer,sources# LLM要求调用搜索工具fortcinmsg.tool_calls:args=json.loads(tc.function.arguments)results=self.store.search(args["query"])context="\n\n".join(f"[片段{i+1}] 出自{m['source']}{m['idx']}段:\n{doc}"fori,(doc,m)inenumerate(results))sources.extend(m["source"]for_,minresults)messages.append({"role":"tool","tool_call_id":tc.id,"content":context})

多轮记忆就靠self.history列表。第二轮"能顺延吗"发出时,历史里有第一轮的问答,LLM知道"顺延"的主语是年假,会自动用"年假 顺延"作为检索关键词,不需要用户重复。

跑起来

agent=DocQAAgent()# 首次运行,上传一份制度文档agent.store.add(open("公司制度FAQ.txt",encoding="utf-8").read(),"公司制度FAQ.txt")# 开始问答whileTrue:q=input("\n👤 你: ").strip()ifq=="exit":breakanswer,src=agent.ask(q)print(f"🤖{answer}")ifsrc:print(f"📌 引用来源:{', '.join(set(src))}")
👤 你: 年假有几天? 🤖 入职满1年5天,满3年10天,满5年15天。 📌 引用来源:公司制度FAQ.txt 👤 你: 能顺延吗? 🤖 年假当年未休完可顺延至次年3月31日。 📌 引用来源:公司制度FAQ.txt 👤 你: 你好 🤖 你好!我是公司制度问答助手,可以问我年假、报销、请假等问题。

第三轮"你好"没有触发搜索,LLM直接回复了——这就是Function Calling的价值,Agent自己判断什么时候需要查文档。

跟Day07朴素RAG的区别

维度Day07 朴素RAGDay16 文档问答Agent
检索触发每问必搜LLM自己判断要不要搜
检索词用户原句直接搜LLM改写为更精准的关键词
多轮追问不支持,每轮独立带历史,理解"它"“这个”“能顺延吗”
引用来源标注文件名和段落
打招呼/闲聊也会去搜文档LLM直接回复不搜

完整版支持PDF和Word文档上传(PyPDF2/python-docx解析)、按文档类型分集合检索、回答后LLM自评置信度,放在CSDN下载区。

GLM-4.7-Flash永久免费,embedding-3费用极低(一篇文档入库约几分钱),ChromaDB开源免费。
下一篇Day17做研究助手:给一个课题,Agent自主拆解、搜索、反思查漏、最后生成完整报告。


本文由「梅雅达编程笔记」原创,首发CSDN。
AI Agent实战系列共18篇,从Function Calling到多Agent协作,全程用免费模型GLM-4.7-Flash,点个关注不迷路✨
CSDN博客:https://blog.csdn.net/2601_96428997/

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 16:51:09

uniapp 微信小程序请求规范

思路是首先是main.js需要将请求接口全部放在vuex里面// 初始API配置initApi(store);export const initApi (store) > {for (let i 0; i < API.length; i) {let item API[i];// 用item.name名作为作为调用方法/*return response Promise*/// 设置请求前缀名item.url $…

作者头像 李华
网站建设 2026/9/8 16:50:26

GitNexus架构解析:如何用工程化机制让AI改代码不失控

开头&#xff1a;当AI把“改一行代码”变成“炸掉整个服务” 如果你最近几年一直在用AI写代码、改代码&#xff0c;大概率碰过这种场景&#xff1a;让AI修一个很小的bug&#xff0c;它非常自信地给你改了一行配置&#xff0c;你觉得没问题就合了&#xff0c;结果线上服务直接起…

作者头像 李华
网站建设 2026/9/8 16:49:41

一串“9”为何能炸穿系统?边界值测试与输入校验实战解析

先问你一个问题&#xff1a;如果你的用户注册接口收到一个字段叫 phone&#xff0c;值是一串99999999999999&#xff0c;你的第一反应是什么&#xff1f;拉黑&#xff1f;直接报错&#xff1f;还是默默存库&#xff1f;我之所以想写这篇东西&#xff0c;是因为这串看起来像键盘…

作者头像 李华
网站建设 2026/9/8 16:49:17

Dirty Coding Tricks:四个偏门技巧提升代码效率与性能

Dirty Coding Tricks 这个系列写到现在已经是第三篇了&#xff0c;后台一直有人在问同一个问题&#xff1a;到底什么样的代码才算 dirty&#xff1f;我的回答一直没变——不是让你写烂代码&#xff0c;不是教你把项目搞成一坨只有自己能看懂的浆糊&#xff0c;而是指那些第一眼…

作者头像 李华
网站建设 2026/9/8 16:47:01

优化器选型与调参实战:从AdamW到Lion的避坑指南

上个月我把一个视觉模型的训练任务从 AdamW 切到另一个优化器&#xff0c;loss 曲线肉眼可见地往下掉&#xff0c;可 validation 指标却纹丝不动。同事看了一眼训练日志&#xff0c;随口说了一句&#xff1a;“这 Optimizer 挺有意思&#xff0c;不过你是不是没搞清楚它到底在优…

作者头像 李华