Tying the knot between two modules

It’s only slightly more tricky:

module type BaseT = sig
  type base
  val pr : base -> string
end

module type AbstT = sig
  type abst
  val pra : abst -> string
end

module BaseF (Abst : AbstT) = struct
  type base = Ident of string | Abst of Abst.abst list

  let pr = function
    | Ident s -> s
    | Abst al -> String.concat ", " (List.map Abst.pra al)
end

module AbstF (Base : BaseT) = struct
  type abst = Foo of Base.base

  let pra = function
    | Foo b -> Printf.sprintf "Foo (%s)" (Base.pr b)
end

module rec Base : BaseT = BaseF(Abst)
and Abst : AbstT = AbstF(Base)

It’s also possible to simply make Base and Abst mutually recursive (without any functor), if you’re ok with having both implementations in the same file. And the middle-ground option, with one file defining a functor and the other one containing the recursive binding works too.
The main thing you should be careful about with recursive modules is the “safe module” constraint: every recursive cycle in a recursive module binding must contain at least one definition which only exports types (including module types if you want) and functions. (Or submodules with the same constraints. Classes are allowed too.)
Example:

module rec M : sig
  type t
  val compare : t -> t -> int
  val default : t
end = struct
  type t = T | S of S.t
  let compare x y = 0
  let default = S (S.singleton T)
end and S : Set.S with type elt = M.t = Set.Make(M)

The compiler will complain about the cycle M -> S -> M, in which neither M nor S is safe (M.default is not a function, and S.empty isn’t either). In this particular case you can fix it by making default a function (val default : unit -> t and let default () = ...), but sometimes it will not work.