with-output-to-string seems to be a good fit for your problem. It makes a stream you can write to in a loop, and returns whatever you wrote as a string:
(with-output-to-string (s)
(loop with first = t
for (a . b) in params
if first
do (setq first nil)
(princ "?" s)
else
do (princ "&" s)
end
do (format s "~a=~a" a b)))