ARTICLE DETAIL

资讯详情

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

用PHP搭建Excel智能问答后台:多模型路由与数据底座实战

用PHP搭建Excel智能问答后台:多模型路由与数据底座实战 简介面向需要将AI能力集成到业务系统中的开发者这套基于PHP后台调用AI开放平台的Excel问答Chat系统源码包整合了deepseek、文心一言、通义千问三大模型可对Excel数据进行自然语言问答。包体共1107个文件压缩后1.92MB以853个PHP核心逻辑文件为主搭配config配置、functions函数、json数据接口及md/txt说明文档。已有321人学习/下载适合具备基础PHP知识、希望学习多平台AI接口对接或快速搭建类似系统的中高级开发者。源码完整呈现了从前端Chat交互、后台服务转发到多模型调用与Excel数据解析的完整链路可直接借鉴其接口封装、权限配置、异常处理等实践细节也可作为企业客服、数据分析、知识库问答等场景的基线工程。1. 用Cursor/Trae搭一个能问Excel的PHP多模问答后台运营同事把一张销售明细表丢过来第一句话是能不能在这个Excel上做个问答我问什么它答什么。从技术上拆开看这件事的本质是两层一层是把Excel里的数据变成系统能检索的东西另一层是让大模型理解华东区第二季度环比跌了多少这种口语化问题。用PHP做后台去承接这两层比想象中靠谱。PHP的执行成本和部署成本都低现有业务服务器上直接挂个接口就能跑不需要单独维护Python常驻进程。这套系统用Cursor和Trae把工程骨架拉起来以后核心工作其实是三块封装AI开放平台接口、设计Excel数据底座、写多模型路由策略。deepseek负责推理计算文心一言偏语义理解通义千问偏文本生成组合起来能覆盖大部分问答场景。下面按实际开发顺序拆解代码可以直接落到项目里改。2. 为什么是PHP后台而不是Python服务以及AI开放平台接口的调用形态2.1 PHP做AI网关的合理性很多团队一听说要接大模型第一反应是上Python。但对于企业内部工具、报表问答这类场景Python带来的收益并没有想象中大。Python的优势在数据处理生态和异步框架但代价是部署链路变长uwsgi、gunicorn、虚拟环境、依赖锁定任何一个环节出问题都会卡住交付。PHP没有这些负担php-fpm跑起来就是服务一个curl扩展就能覆盖AI开放平台接口的调用需求。另一个现实因素是团队的技术栈。做Excel问答Chat必然要处理表格导入、文件上传、权限控制这些在PHP里都有成熟方案PhpSpreadsheet读Excel、预处理语句写库、Session管用户状态全部在同一个语言体系内完成。后台服务的核心不是模型本身而是把问题转成请求、把请求发出去、把结果整理回来PHP做这件事完全够用。用Cursor生成的初始化代码可以直接在这个基础上改不需要推倒重来。2.2 三家AI开放平台接口的协议差异与统一封装deepseek、文心一言、通义千问三家平台的接口协议并不完全一致。deepseek的接口是OpenAI兼容格式直接用/chat/completions路径加Bearer Token就能调通义千问也提供了兼容OpenAI的端点用DashScope的API Key放到Authorization头里文心一言相对特殊老版本需要先请求token endpoint换取access_token再带着token访问对话接口。做一个统一配置层把差异收敛在配置文件里是最省事的做法。// config/ai_providers.php return [ deepseek [ base_url getenv(DEEPSEEK_API_BASE), api_key getenv(DEEPSEEK_API_KEY), model deepseek-chat, timeout 30, ], wenxin [ token_url getenv(WENXIN_TOKEN_URL), api_key getenv(WENXIN_API_KEY), secret getenv(WENXIN_SECRET), model ernie-bot-turbo, auth_type oauth, timeout 60, ], qwen [ base_url getenv(DASHSCOPE_API_BASE), api_key getenv(DASHSCOPE_API_KEY), model qwen-plus, timeout 60, ], ];这段配置把所有模型差异集中到一处。auth_type字段标记文心一言走OAuth流程deepseek和通义千问直接放API Key。密钥全部从环境变量读取避免config文件被提交到Git仓库后泄露。注意timeout字段deepseek的推理响应快文心和通义在生成长文本时可能超过30秒需要单独放大。2.3 curl封装统一请求入口配置层做好以后写一个统一的Provider类来处理请求。这个类要做三件事按照provider类型组装请求头、执行curl调用、统一返回结构。重试逻辑也放在这里但要注意重试前必须判断上一次失败的原因网络超时可以重试鉴权失败重试多少次都是白费。class AiProvider { public function request(string $provider, array $messages): array { $conf $this-config[$provider]; if (($conf[auth_type] ?? ) oauth) { $conf[access_token] $this-getWenxinToken($conf); $authHeader Authorization: Bearer . $conf[access_token]; } else { $authHeader Authorization: Bearer . $conf[api_key]; } $url rtrim($conf[base_url], /) . /chat/completions; $payload [ model $conf[model], messages $messages, temperature $messages[temperature] ?? 0.2, ]; if (isset($messages[max_tokens])) { $payload[max_tokens] $messages[max_tokens]; } $ch curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_POSTFIELDS json_encode($payload), CURLOPT_HTTPHEADER [ Content-Type: application/json, $authHeader, ], CURLOPT_TIMEOUT $conf[timeout] ?? 30, CURLOPT_RETURNTRANSFER true, ]); $resp curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode ! 200) { return [code $httpCode, error provider request failed]; } $result json_decode($resp, true); return [ code 0, content $result[choices][0][message][content] ?? , ]; } }这段代码暴露了三个关键设计点。第一所有provider共用一个入口上层业务只关心$provider和$messages不需要知道文心和通义的接口差异。第二temperature放进$messages里传入上层可以根据问题类型动态调整聚合计算类问题用低温度保证准确解释类问题用稍高的温度让表达更自然。第三返回结构固定为code content这样调用方不需要try-catch整个请求链路统一判断code即可。3. Excel问答Chat的数据底座从xlsx到可检索的结构化表3.1 数据导入PhpSpreadsheet读取和入库Excel问答的前提是数据能被程序理解。直接让模型读xlsx文件不现实token消耗大、命中率低而且Excel里经常有合并单元格和格式备注。常见的做法是把Excel解析后存入数据库查询时只带上相关的行数据。用PhpSpreadsheet读取Excel文件把每一行拆成字段名-值的键值对写入MySQL这样任意结构的表格都能入库不依赖固定列名。use PhpOffice\PhpSpreadsheet\IOFactory; $spreadsheet IOFactory::load($uploadedFilePath); $sheet $spreadsheet-getActiveSheet(); $rows $sheet-toArray(null, true, true, true); $headerMaps $this-loadAliasMap($rows[1]); $pdo-beginTransaction(); $batchId $this-createBatch($pdo, $fileName); $stmt $pdo-prepare( INSERT INTO excel_dataset (batch_id, row_no, col_key, col_value) VALUES (?, ?, ?, ?) ); foreach (array_slice($rows, 1) as $rowNo $row) { foreach ($headerMaps as $colIndex $fieldKey) { $value $row[$colIndex] ?? ; if ($value || $value null) { continue; } $stmt-execute([$batchId, $rowNo, $fieldKey, mb_substr($value, 0, 500)]); } } $pdo-commit();这里的核心是行转键值对存储。excel_dataset表用batch_id区分每次上传的文件版本col_key存标准化字段名col_value存原始值。这样设计有两个好处一是不会因为Excel列名变化而改表结构二是在问答阶段可以按字段名精确检索。注意mb_substr截断超长文本避免某个单元格塞入大段备注拖慢查询。3.2 中文表头与问题意图的映射Excel的表头是给运营看的模型不一定认识。比如月份和时间可能指向同一个字段GMV和销售金额在语义上等价。所以导入时要维护一张别名映射表把Excel表头归一化成系统内的标准字段名。这个映射表可以用配置文件维护也可以定期让模型帮忙补充。// config/schema_aliases.php return [ date [月份, 时间, 日期, 月], region [区域, 地区, 大区, 市场], product [产品, 商品, sku, 品名], sales_amount [销售金额, 销售额, gmv, 营收, 成交额], sales_qty [销量, 数量, 件数, 销售数量], profit [利润, 毛利, 净利润], ];问题进来以后先用正则和别名表做一次轻量提取。比如用户问华东区6月的销售额正则能抓到华东区和6月别名表把它们映射成region和date。这套浅层解析先兜底后续再交给大模型补全复杂条件。这样做的意义在于减少无效的API调用纯关键词命中就能解决的检索问题没必要再发一次模型请求。3.3 自然语言转查询条件的Prompt工程当浅层解析覆盖不了复杂问题比如对比华东和华南上半年的毛利率差异就需要让模型把自然语言转成结构化查询条件。这里的关键是不能让模型直接拼接SQL而是让模型输出一个JSON结构后端再做白名单校验。Prompt要写清楚允许使用的字段列表和输出格式约束。你是数据分析助手。以下是这个Excel数据集的标准字段列表 date, region, product, sales_amount, sales_qty, profit 把用户问题转换成JSON查询条件不要输出任何其他文字。 JSON结构{filters: [{field: region, op: in, value: [华东, 华南]}], metrics: [profit], time_range: [2024-01-01, 2024-06-30]} 用户问题对比华东和华南上半年的毛利率差异模型返回JSON以后后端要逐个字段校验field必须在白名单内op只能是eq、in、gt、lt、between这几类。校验通过后把这个JSON转成SQL查询或直接检索excel_dataset表。这一步把模型的自由度限制在可控范围内AI生成的脏数据永远不会直接进入SQL语句。4. Chat接口与多模型路由deepseek、文心、通义怎么分工4.1 Chat接口完整请求链路Chat接口的完整链路是用户输入问题、本地意图分类、检索相关Excel数据、组装上下文、按场景路由模型、返回结果。意图分类先用关键词规则打底包含多少、计算、环比、同比归为聚合类包含为什么、怎么看、解释归为解释类剩下的按直接查询处理。这一步决定了后续模型路由和上下文拼接的方式。public function answer(string $question): array { $intent $this-intentRouter-classify($question); $retrieved $this-retrieval-fetch($question, $intent[type]); $context $this-buildContext($retrieved); $messages [ [role system, content 你是数据分析助手基于给定的表格数据回答], [role user, content $question . \n\n数据\n . $context], ]; $providers $this-routeModels($intent[type]); $candidates []; foreach ($providers as $provider) { $candidates[] $this-ai-request($provider, $messages); } return $this-mergeResult($candidates, $intent[type]); }这里的routeModels根据问题类型返回模型列表。直接查询只走deepseek聚合计算走deepseek和文心一言做交叉验证开放解释走通义千问生成完整回答。buildContext会把检索到的数据行拼成文本放进Prompt的user消息里数据多的时候只取前50行外加聚合结果摘要。上下文长度直接影响token成本和回答质量需要做截断控制。4.2 多模型结果合并与一致性多模型路由带来一个直接问题两三个模型返回了不同答案怎么办。聚合计算类问题两个模型的结论如果不一致直接返回会让用户困惑。常见的做法是加一层简单投票策略要求两个模型结果一致才返回如果不一致把两个结果同时展示并标注差异点让用户自己判断。开放解释类问题不需要强一致模型语言风格不同反而带来多样性单模型返回即可。private function mergeResult(array $candidates, string $type): array { if (count($candidates) 1) { return [content $candidates[0][content] ?? , validated true]; } $similar similar_text( $candidates[0][content] ?? , $candidates[1][content] ?? , $percent ); if ($percent 70) { return [content $candidates[0][content], validated true]; } return [ content $candidates[0][content] . \n\n另一个模型的结果\n . $candidates[1][content] . , validated false, ]; }这段代码用similar_text函数计算两个模型回答的相似度相似度超过70%就认为结果一致返回第一个模型的答案否则两个答案拼接展示并在validated字段标记未通过一致性校验。similar_text的性能对短文本足够快聚合计算类问题的答案一般控制在几百字以内不会成为瓶颈。前端拿到这个字段以后可以展示一个结果未经校验的提示降低误导风险。4.3 限流、缓存与队列AI开放平台接口都有速率限制三个模型的限制策略还不一样直接在请求层做限流容易把复杂度堆在业务代码里。常见做法是引入Redis队列把模型请求扔进按provider分桶的队列里消费端限速拉取。deepseek的RPM限制通常比文心和通义宽松队列参数要按provider单独配置。// Redis队列消费脚本worker/deepseek.php $redis new Redis(); $redis-connect(127.0.0.1, 6379); while ($job $redis-lPop(queue:ai:deepseek)) { $job json_decode($job, true); $resp (new AiProvider())-request(deepseek, $job[messages]); $redis-lPush(queue:ai:result, json_encode([ track_id $job[track_id], response $resp, ])); usleep(200000); // 每个请求间隔200ms控制QPS }队列消费者用lPop取任务处理完把结果推到结果队列。usleep(200000)让每次请求间隔200毫秒把QPS压在5以内这是个保守但安全的初始值。Excel问答属于低并发场景5 QPS足够支撑一个几十人的团队使用。如果某个provider的key额度很低这个间隔还要拉大到500毫秒。缓存层面同一个Excel批次文件的内容查询结果可以按batch_id 问题hash缓存2小时避免重复提问反复消耗API额度。5. 进阶验证与真实落地技巧5.1 用三类提问验证系统质量系统上线前要有一套自己的验证集不能靠感觉判断问答质量。按照路由策略把测试问题分为三类每类随机抽10个手工标注期望答案再对比系统输出。直接查询类要验证检索的准确性聚合计算类要验证数值计算的正确性开放解释类要验证回答是否引用了数据而非编造。类型示例问题期望结果直接查询华东区的产品列表有哪些从datasets返回产品字段去重结果聚合计算华东区第一季度销售总额数值精确且计算口径符合预期开放解释为什么华东区的毛利率在下降引用利润与销售额数据给出解释跑验证集的时候记录每个问题的模型参数、回答耗时、是否通过校验三个指标。如果聚合计算类问题频繁出现两个模型结果不一致先检查Prompt里的数据上下文是否足够再检查temperature是不是调得太高。数值计算类问题temperature应固定为0.1让模型没有随机发挥空间。5.2 关键参数实测几个参数在实际调试中对效果影响最大第一是temperature聚合类0.1、直接查询0.2、解释类0.4超过0.5之后模型编造数据的概率显著上升第二是max_tokensExcel问答的答案一般不需要长篇大论500 token足够覆盖所有回答过大会拖慢响应第三是超时时间deepseek用30秒文心和通义至少给60秒。以下是经过多轮测试的参数表参数deepseek文心一言通义千问temperature0.10.20.4max_tokens500500800timeout30s60s60s重试次数212注意文心一言的token endpoint调用会有短时缓存一般5分钟内有效不要每次请求都去换取新token否则会在鉴权环节拖慢整体响应。5.3 线上排查链路与日志线上问答系统最怕的是用户说答案不对但开发环境复现不出来。解决方法是给每条请求打一个track_id从用户提问到模型返回全程透传同时记录完整的请求和响应日志。我一般会建一张ai_qa_logs表字段包括track_id、question、intent_type、provider、prompt_tokens、completion_tokens、latency_ms、response_content、error_code。排查问题时直接用track_id把整条链路捞出来看到底是意图分类错了、检索漏了数据还是模型返回了空值。最后你会发现真正拉开项目质量差距的往往不是选用哪个模型而是数据和参数之间的配合。先只开deepseek一条链路把问答跑通再逐步打开文心和通义的投票开关每次只调整一个变量对照验证集看指标变化。不同业务数据的特点差异很大先让系统在单模型下稳定工作再谈多模型协同落地速度会快得多。本文还有配套的精品资源点击获取
返回列表